Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/qs_codec/decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ def _parse_array_value(value: t.Any, options: DecodeOptions, current_list_length
- When ``list_limit`` is negative:
* if ``raise_on_limit_exceeded=True``, **any** list-growth operation here (e.g., comma-splitting)
raises immediately;
* if ``raise_on_limit_exceeded=False`` (default), comma-splitting still returns a list; numeric
* if ``raise_on_limit_exceeded=False`` (default), comma-splitting is allowed here and is normalized
later in ``_parse_query_string_values`` (which applies overflow conversion when needed). Numeric
bracket indices are handled later by ``_parse_object`` (where negative ``list_limit`` disables
numeric-index parsing only).

Expand Down Expand Up @@ -176,7 +177,10 @@ def _parse_query_string_values(value: str, options: DecodeOptions) -> t.Dict[str
* Decode key/value via ``options.decoder`` (default: percent-decoding using the selected ``charset``).
Keys are passed with ``kind=DecodeKind.KEY`` and values with ``kind=DecodeKind.VALUE``; a custom decoder
may return the raw token or ``None``.
* Apply comma-split list logic to values (handled here). Index-based list growth from bracket segments is applied later in ``_parse_object``. When ``list_limit < 0`` and ``raise_on_limit_exceeded=True``, any comma-split that would increase the list length raises immediately; otherwise the split proceeds.
* Apply comma-split list logic to values (handled here). Index-based list growth from bracket segments
is applied later in ``_parse_object``. When ``comma=True`` yields a list longer than ``list_limit``,
the value either raises (``raise_on_limit_exceeded=True``) or is normalized through overflow conversion
(``raise_on_limit_exceeded=False``), mirroring indexed/bracket list-limit handling.
* Interpret numeric entities for Latin-1 when requested.
* Handle empty brackets ``[]`` as list markers (wrapping exactly once).
* Merge duplicate keys according to ``duplicates`` policy.
Expand Down Expand Up @@ -285,6 +289,14 @@ def _parse_query_string_values(value: str, options: DecodeOptions) -> t.Dict[str
if options.parse_lists and "[]=" in part:
val = [val]

# Enforce comma-split list limits consistently with other list construction paths.
if options.comma and isinstance(val, list) and len(val) > options.list_limit:
Comment on lines 289 to +293

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Check comma overflow before applying [] list wrapping

With inputs like a[]=1,2,3,4 and DecodeOptions(comma=True, list_limit=3, raise_on_limit_exceeded=False), the overflow check is bypassed because val is wrapped to [val] before the new limit guard runs, making len(val) equal to 1. This means over-limit comma-parsed payloads that use []= still return oversized inner lists instead of being normalized to overflow-object semantics, so the list-limit enforcement added here is incomplete for a common query form.

Useful? React with 👍 / 👎.

if options.raise_on_limit_exceeded:
raise ValueError(
f"List limit exceeded: Only {options.list_limit} element{'' if options.list_limit == 1 else 's'} allowed in a list."
)
val = Utils.combine([], val, options)
Comment on lines +293 to +298
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This enforcement only checks isinstance(val, list). Custom decoders (or other internal paths) can yield tuples as well (the rest of this function already treats (list, tuple) as list-like), which would bypass the comma list-limit enforcement. Consider checking isinstance(val, (list, tuple)) (and normalize tuples the same way) so oversized tuple values are handled consistently.

Suggested change
if options.comma and isinstance(val, list) and len(val) > options.list_limit:
if options.raise_on_limit_exceeded:
raise ValueError(
f"List limit exceeded: Only {options.list_limit} element{'' if options.list_limit == 1 else 's'} allowed in a list."
)
val = Utils.combine([], val, options)
if options.comma and isinstance(val, (list, tuple)):
val_sequence = list(val)
if len(val_sequence) > options.list_limit:
if options.raise_on_limit_exceeded:
raise ValueError(
f"List limit exceeded: Only {options.list_limit} element{'' if options.list_limit == 1 else 's'} allowed in a list."
)
val = Utils.combine([], val_sequence, options)

Copilot uses AI. Check for mistakes.

existing: bool = key in obj

# Combine/overwrite according to the configured duplicates policy.
Expand Down
69 changes: 69 additions & 0 deletions tests/unit/decode_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1370,6 +1370,75 @@ def test_list_limit(
else:
assert decode(query, options) == expected

@pytest.mark.parametrize(
"query, options, expected, raises_error, expect_overflow",
[
pytest.param(
"a=1,2,3",
DecodeOptions(comma=True, list_limit=5),
{"a": ["1", "2", "3"]},
False,
False,
id="comma-within-list-limit",
),
pytest.param(
"a=1,2,3,4",
DecodeOptions(comma=True, list_limit=3),
{"a": {"0": "1", "1": "2", "2": "3", "3": "4"}},
False,
True,
id="comma-over-list-limit-converts-to-overflow",
),
pytest.param(
"a=1,2,3,4",
DecodeOptions(comma=True, list_limit=3, raise_on_limit_exceeded=True),
None,
True,
False,
id="comma-over-list-limit-raises",
),
pytest.param(
"a=1,2,3",
DecodeOptions(comma=True, list_limit=3),
{"a": ["1", "2", "3"]},
False,
False,
id="comma-at-list-limit-stays-list",
),
Comment on lines +1373 to +1407
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new comma/list_limit parity tests don’t cover the []-wrapping case (e.g. a[]=1,2,3,4 with comma=True, list_limit=3, raise_on_limit_exceeded=False). That path is important because the tokenizer wraps the comma-split list (val = [val]), which can mask an over-limit inner list unless explicitly enforced. Adding this case would prevent regressions and ensure the intended CVE/parity fix applies when comma-splitting is combined with empty-bracket list markers.

Copilot generated this review using guidance from repository custom instructions.
],
)
def test_comma_list_limit_parity(
self,
query: str,
options: DecodeOptions,
expected: t.Optional[t.Mapping[str, t.Any]],
raises_error: bool,
expect_overflow: bool,
) -> None:
if raises_error:
with pytest.raises(ValueError, match="List limit exceeded"):
decode(query, options)
return

result = decode(query, options)
assert result == expected
if expect_overflow:
assert isinstance(result["a"], OverflowDict)
else:
assert isinstance(result["a"], list)

def test_comma_list_limit_raises_when_decoder_returns_oversized_list(self) -> None:
def _decoder(s: t.Optional[str], charset: t.Optional[Charset], *, kind: DecodeKind = DecodeKind.VALUE) -> t.Any:
if kind is DecodeKind.VALUE and s == "1":
return ["x", "y"]
return DecodeUtils.decode(s, charset=charset, kind=kind)

with pytest.raises(ValueError, match="List limit exceeded"):
decode(
"a=1",
DecodeOptions(comma=True, list_limit=1, raise_on_limit_exceeded=True, decoder=_decoder),
)


# --- Additional tests for decoder kind and parser state isolation ---

Expand Down