Skip to content

🐛 fix decode: enforce list_limit on comma-parsed values (qs v6.14.2 parity)#41

Closed
techouse wants to merge 3 commits intomainfrom
fix/qs-js-ghsa
Closed

🐛 fix decode: enforce list_limit on comma-parsed values (qs v6.14.2 parity)#41
techouse wants to merge 3 commits intomainfrom
fix/qs-js-ghsa

Conversation

@techouse
Copy link
Owner

@techouse techouse commented Feb 17, 2026

Summary

This PR aligns qs_codec.decode with upstream qs behavior for comma-parsed values and list limits (GHSA-w7fw-mjwx-w883 / CVE-2026-2391 parity).

Previously, when comma=True and list_limit was exceeded, the parser could still return a Python list when raise_on_limit_exceeded=False.
Now, over-limit comma-split values are normalized to overflow-object semantics (numeric-key dict/OverflowDict) instead of remaining oversized lists.

Motivation

qs fixed this class of issue in v6.14.2 by enforcing arrayLimit on comma-parsed values.
As this project is a port of qs, this change keeps behavior and safety expectations in sync.

Changes

  • src/qs_codec/decode.py

  • Added comma list-limit enforcement in _parse_query_string_values after value decoding and [] wrapping.

  • If options.comma and decoded val is a list and len(val) > options.list_limit:

  • raise_on_limit_exceeded=True -> raise ValueError (existing message format).

  • raise_on_limit_exceeded=False -> normalize with Utils.combine([], val, options) to produce overflow-object semantics.

  • Updated related docstrings/comments to reflect the new normalization behavior.

  • tests/unit/decode_test.py

  • Added parameterized parity tests for comma + list_limit:

  • within limit stays list

  • over limit converts to overflow object

  • over limit with raise_on_limit_exceeded=True raises

  • exactly at limit stays list

  • Added targeted regression test to hit the decoder-produced oversized list raise path (restores 100% coverage).

Behavioral example

  • Input: a=1,2,3,4
  • Options: DecodeOptions(comma=True, list_limit=3, raise_on_limit_exceeded=False)
  • Before: {"a": ["1", "2", "3", "4"]}
  • After: {"a": {"0": "1", "1": "2", "2": "3", "3": "4"}} (OverflowDict semantics)

Validation

  • pytest -q tests/unit/decode_test.py -k "comma and list_limit"
  • pytest -q tests/unit/decode_test.py
  • pytest -q
  • pytest -q --cov=src/qs_codec --cov-report=term-missing

Results:

  • 926 passed
  • TOTAL ... 100% coverage

Compatibility

  • No public API signature changes.
  • No intentional behavior changes outside comma/list-limit enforcement parity.

References

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced list limit enforcement for comma-separated query values. Now consistently raises errors or normalizes oversized lists based on configuration, aligning with indexed list handling.
  • Tests

    • Added unit tests for comma-delimited list limit behavior, including overflow scenarios, error conditions, and limit enforcement across various configurations.

@techouse techouse self-assigned this Feb 17, 2026
@techouse techouse added the bug Something isn't working label Feb 17, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

The changes add enforcement of list limits to comma-split values in query string decoding. When comma-splitting produces a list exceeding the limit, it either raises an error (if raise_on_limit_exceeded=True) or normalizes via Utils.combine(). New unit tests validate this behavior across various parameter combinations.

Changes

Cohort / File(s) Summary
Comma-split list limit enforcement
src/qs_codec/decode.py
Added enforcement block in parse_query_string_values to apply comma-split list limits; raises ValueError when raise_on_limit_exceeded=True and list exceeds limit, otherwise normalizes via Utils.combine(). Documentation expanded in parse_array_value and parse_query_string_values to describe new behavior.
Unit tests for comma list limits
tests/unit/decode_test.py
Added parameterized test_comma_list_limit_parity and test_comma_list_limit_raises_when_decoder_returns_oversized_list to validate comma-split list limit enforcement across various configurations and edge cases.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Possibly related PRs

Suggested labels

test

Poem

🐰 A comma here, a limit there,
Lists are split with utmost care,
When bounds are crossed, we validate,
Normalize or raise—it's fate!
Testing brings our peace of mind,
Comma harmony we'll find! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: enforcing list_limit on comma-parsed values to achieve qs v6.14.2 parity, which matches the primary focus of the changeset.
Description check ✅ Passed The description comprehensively addresses all template sections: summary with issue context, clear type of change identification, testing validation with specific test counts and coverage metrics, and completion of relevant checklist items.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/qs-js-ghsa

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production
Copy link

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
+0.00% (target: -1.00%) 100.00%
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (77fac3c) 1247 1247 100.00%
Head commit (62357cb) 1251 (+4) 1251 (+4) 100.00% (+0.00%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#41) 4 4 100.00%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

See your quality gate settings    Change summary preferences

1 similar comment
@codacy-production
Copy link

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
+0.00% (target: -1.00%) 100.00%
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (77fac3c) 1247 1247 100.00%
Head commit (62357cb) 1251 (+4) 1251 (+4) 100.00% (+0.00%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#41) 4 4 100.00%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

See your quality gate settings    Change summary preferences

@codecov
Copy link

codecov bot commented Feb 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (77fac3c) to head (62357cb).

Additional details and impacted files
@@            Coverage Diff            @@
##              main       #41   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           17        17           
  Lines         1247      1251    +4     
=========================================
+ Hits          1247      1251    +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/qs_codec/decode.py (1)

292-298: Duplicate raise path for comma-split values — consider removing the earlier one.

When raise_on_limit_exceeded=True and a comma-split in _parse_array_value (line 152) produces an oversized list, the error is raised there before this block is reached. This block's raise path (lines 294–297) therefore only fires for lists produced by a custom decoder (not by comma-splitting in _parse_array_value). That's correct and the tests confirm it.

However, the redundancy between line 152 and line 294 for the comma-split + raise case means there are now two places defining the same error message for the same semantic check. If the message format ever changes, both sites must be updated in lockstep.

Consider consolidating: let _parse_array_value always return the split list (drop lines 152–155) and let this single enforcement block handle both raise and overflow-conversion for all list sources uniformly. This would also simplify the _parse_array_value docstring.

♻️ Sketch: consolidate the raise into the single enforcement block

In _parse_array_value (lines 150–156), remove the raise guard so it always returns the split list:

     if isinstance(value, str) and value and options.comma and "," in value:
         split_val: t.List[str] = value.split(",")
-        if options.raise_on_limit_exceeded and len(split_val) > options.list_limit:
-            raise ValueError(
-                f"List limit exceeded: Only {options.list_limit} element{'' if options.list_limit == 1 else 's'} allowed in a list."
-            )
         return split_val

The existing block at lines 292–298 already handles both raise and overflow-conversion for any list that exceeds list_limit, so no other changes are needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/qs_codec/decode.py` around lines 292 - 298, Remove the duplicate raise in
_parse_array_value so comma-split always returns the split list and let the
single enforcement block (the if options.comma and isinstance(val, list) ...
using options.list_limit, options.raise_on_limit_exceeded and Utils.combine) be
the sole place that enforces list_limit and either raises or combines;
specifically, delete the early raise in _parse_array_value and rely on the
existing enforcement in the block that checks options.comma/len(val) against
options.list_limit to handle both comma-split and custom-decoder lists
uniformly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/qs_codec/decode.py`:
- Around line 292-298: Remove the duplicate raise in _parse_array_value so
comma-split always returns the split list and let the single enforcement block
(the if options.comma and isinstance(val, list) ... using options.list_limit,
options.raise_on_limit_exceeded and Utils.combine) be the sole place that
enforces list_limit and either raises or combines; specifically, delete the
early raise in _parse_array_value and rely on the existing enforcement in the
block that checks options.comma/len(val) against options.list_limit to handle
both comma-split and custom-decoder lists uniformly.

@techouse techouse requested a review from Copilot February 17, 2026 00:31
@techouse
Copy link
Owner Author

@codex review

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates qs_codec.decode to match upstream qs v6.14.2 behavior by enforcing list_limit for comma-parsed values, converting over-limit comma-split arrays into overflow-object semantics (or raising when configured), and adds unit tests to lock in the parity behavior.

Changes:

  • Enforce list_limit when comma=True produces a list value, raising or normalizing to overflow-object semantics.
  • Update decoder docstrings/comments to describe the new comma/list-limit handling.
  • Add unit tests covering comma + list_limit within-limit, at-limit, overflow-normalization, and raise-on-overflow paths (plus a decoder-regression test).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/qs_codec/decode.py Adds comma-split list-limit enforcement/normalization during query tokenization.
tests/unit/decode_test.py Adds parity/regression tests for comma + list_limit behaviors.

Comment on lines +287 to +298
# If the pair used empty brackets syntax and list parsing is enabled, force an array container.
# Always wrap exactly once to preserve list-of-lists semantics when comma splitting applies.
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:
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)
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 enforcement runs after the []= wrapping (val = [val]). For inputs like a[]=1,2,3,4 with comma=True, list_limit=3, raise_on_limit_exceeded=False, the comma split creates an inner list of 4 items but wrapping turns it into an outer list of length 1, so this check won’t trigger and the over-limit comma-parsed list will still be returned (nested) without overflow normalization. To enforce list_limit on comma-parsed values consistently, apply the over-limit normalization/raise to the comma-split result before wrapping (or explicitly detect and handle the wrapped-list-of-list case).

Copilot uses AI. Check for mistakes.
Comment on lines +293 to +298
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)
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.
Comment on lines +1373 to +1407
@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",
),
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.
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 62357cb311

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 289 to +293
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:

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 👍 / 👎.

@techouse techouse closed this Feb 17, 2026
@techouse techouse deleted the fix/qs-js-ghsa branch February 17, 2026 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant