Skip to content

fix: Special characters in list-based where constraints break URL parsing#1134

Open
AdrianCurtin wants to merge 4 commits intoparse-community:masterfrom
AdrianCurtin:fix/where-list-special-characters
Open

fix: Special characters in list-based where constraints break URL parsing#1134
AdrianCurtin wants to merge 4 commits intoparse-community:masterfrom
AdrianCurtin:fix/where-list-special-characters

Conversation

@AdrianCurtin
Copy link
Copy Markdown

@AdrianCurtin AdrianCurtin commented Apr 18, 2026

Problem

QueryBuilder methods that accept a List<dynamic>whereContainedIn, whereNotContainedIn, and whereArrayContainsAll — do not URL-encode String elements before embedding them into the where={...} querystring built by buildQuery().

The resulting string is passed to Uri(query: ...) in getSanitisedUri (parse_utils.dart). Dart's Uri constructor treats &, =, +, and # as valid query sub-delimiters per RFC 3986 and does not percent-encode them when they appear inside the query: argument. As a consequence, a list element like "Peanut Butter & Jelly" ends up on the wire as a literal &, causing the server-side querystring parser (Parse Server / Express / qs) to split the where value at the unescaped delimiter. The JSON becomes malformed and the server rejects the request:

ParseError: Invalid parameter for query:  Jelly"]}}
    at ClassesRouter.optionsFromBody
    ...
  code: 102

Minimal reproducer

final query = QueryBuilder<ParseObject>(ParseObject('Food'))
  ..whereContainedIn('tags', <String>['Peanut Butter & Jelly']);
await query.query(); // => 400 Invalid parameter for query

Fix

Follows the pattern introduced in #866 for the scalar whereEqualTo / whereNotEqualTo / whereStartsWith / whereEndsWith / whereContains / whereContainsWholeWord methods: apply Uri.encodeComponent to each String element at constraint-build time. Non-String elements (numbers, pointers, embedded maps, etc.) are passed through unchanged.

A small private helper _encodeStringElements is added to avoid duplicating the map-and-encode logic across the three call sites.

Tests

Two new tests added to packages/dart/test/src/network/parse_query_test.dart:

  1. Encoding coverage — verifies that &, =, +, and # are percent-encoded in the where={...} string produced by whereContainedIn, whereNotContainedIn, and whereArrayContainsAll, and that the raw unescaped delimiters do not appear inside the JSON values.
  2. Non-String pass-through — verifies that whereContainedIn('nums', [1, 2, 3]) still produces "$in":[1,2,3] with numeric elements intact.

Full dart test suite (208 tests) passes locally.

Related

Summary by CodeRabbit

  • Bug Fixes

    • String values in query builders are now JSON-escaped and percent-encoded so special characters (e.g., &, =, +, #) and escaped quotes/backslashes are transmitted safely, preventing malformed query strings.
    • Non-string list elements (e.g., numbers) are preserved and remain unaffected.
  • Tests

    • Added tests verifying proper encoding/escaping for string inputs and correct handling of non-string list elements.

…arsing

`whereContainedIn`, `whereNotContainedIn`, and `whereArrayContainsAll`
embed String elements into the `where={...}` querystring without URL
encoding. `Uri(query: ...)` leaves `&`, `=`, `+`, and `#` as valid
query sub-delimiters, so an element like "Peanut Butter & Jelly"
causes the server to split the querystring at the literal `&` and
reject the request with `code: 102 Invalid parameter for query`.

Apply `Uri.encodeComponent` to String elements at constraint-build
time, following the pattern introduced in parse-community#866 for the scalar
`whereEqualTo` / `whereContains` family. Non-String elements (numbers,
pointers, etc.) are left untouched.
Copilot AI review requested due to automatic review settings April 18, 2026 18:50
@parse-github-assistant
Copy link
Copy Markdown

🚀 Thanks for opening this pull request! We appreciate your effort in improving the project. Please let us know once your pull request is ready for review.

Tip

  • Keep pull requests small. Large PRs will be rejected. Break complex features into smaller, incremental PRs.
  • Use Test Driven Development. Write failing tests before implementing functionality. Ensure tests pass.
  • Group code into logical blocks. Add a short comment before each block to explain its purpose.
  • We offer conceptual guidance. Coding is up to you. PRs must be merge-ready for human review.
  • Our review focuses on concept, not quality. PRs with code issues will be rejected. Use an AI agent.
  • Human review time is precious. Avoid review ping-pong. Inspect and test your AI-generated code.

Note

Please respond to review comments from AI agents just like you would to comments from a human reviewer. Let the reviewer resolve their own comments, unless they have reviewed and accepted your commit, or agreed with your explanation for why the feedback was incorrect.

Caution

Pull requests must be written using an AI agent with human supervision. Pull requests written entirely by a human will likely be rejected, because of lower code quality, higher review effort and the higher risk of introducing bugs. Please note that AI review comments on this pull request alone do not satisfy this requirement. Our CI and AI review are safeguards, not development tools. If many issues are flagged, rethink your development approach. Invest more effort in planning and design rather than using review cycles to fix low-quality code.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

QueryBuilder string handling now encodes string inputs via two new private helpers. Scalar string parameters use _encodeStringElement; list-based operators map String elements through _encodeStringElements while leaving non-strings unchanged. Tests validating encoding and preservation of non-strings were added.

Changes

Cohort / File(s) Summary
Query builder encoding changes
packages/dart/lib/src/network/parse_query.dart
Introduced _encodeStringElement(String) and _encodeStringElements(List<dynamic>). Switched scalar string query builders (e.g., whereStartsWith, whereEndsWith, whereEqualTo/whereNotEqualTo for strings, whereContains, substring-based queries, whereContainsWholeWord) to _encodeStringElement. Updated list-based operators (whereContainedIn, whereNotContainedIn, whereArrayContainsAll) to map only String items through _encodeStringElements, preserving non-String elements.
Query builder tests
packages/dart/test/src/network/parse_query_test.dart
Added four unit tests asserting that scalar and list-based String values are JSON-escaped then percent-encoded (special chars like &, =, +, #, and escaped "/\), raw delimiter substrings are absent from buildQuery() output, and non-String list elements remain unmodified.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 6 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Engage In Review Feedback ❓ Inconclusive GitHub PR review comments and feedback are not accessible from the repository codebase alone. Provide access to GitHub PR #1134 review comments, discussion threads, or documentation showing how feedback was addressed.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The PR title correctly uses the 'fix:' prefix as required and clearly describes the specific bug being addressed: special characters in list-based where constraints breaking URL parsing.
Description check ✅ Passed The PR description is comprehensive and well-structured, addressing the problem, fix approach, tests added, and related issues, though it does not strictly follow the provided template format.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Check ✅ Passed Changes uniformly apply JSON escaping and percent-encoding to prevent NoSQL-operator injection and URL parsing issues without introducing unsafe patterns.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown

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 fixes URL parsing issues when list-based where constraints contain special characters by percent-encoding string list elements before they are embedded into the where={...} query string.

Changes:

  • Encode String elements passed to whereContainedIn, whereNotContainedIn, and whereArrayContainsAll using Uri.encodeComponent.
  • Add a small private helper to centralize list element encoding logic.
  • Add tests covering encoding of &, =, +, # and verifying non-String list elements are unaffected.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
packages/dart/lib/src/network/parse_query.dart Applies string-element percent-encoding for list-based constraints via a shared helper.
packages/dart/test/src/network/parse_query_test.dart Adds regression tests for special-character encoding and non-String pass-through behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +706 to +710
queryBuilder.whereContainedIn('topics', <dynamic>[
"RFI's & Change Orders",
'Schedule (Impacts, Delays, Inspections)',
]);
queryBuilder.whereNotContainedIn('excluded', <dynamic>['a=b', 'c+d']);
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

These test list literals are typed as <dynamic>, which weakens static checking without being necessary here. Prefer <String>[...] for the string cases (and <int>[...] for the numeric case) so the tests better reflect typical API usage and catch type mistakes at compile time.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/dart/lib/src/network/parse_query.dart`:
- Around line 321-329: The current _encodeStringElements function
percent-encodes the raw string which allows JSON metacharacters (", \, newlines)
to reappear unescaped after query decoding and break server-side JSON parsing;
fix it by JSON-escaping each string before percent-encoding: for each element in
_encodeStringElements, if e is a String call jsonEncode(e) to produce a
quoted/escaped JSON string, strip the surrounding quotes to get the JSON-escaped
inner content, then pass that inner content to Uri.encodeComponent; leave
non-String elements untouched. Update callers/uses (e.g., whereContainedIn,
whereNotContainedIn, whereArrayContainsAll) to rely on this corrected encoding
helper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 47f6b1a0-6bcb-4366-8f79-76a1a86b0ebc

📥 Commits

Reviewing files that changed from the base of the PR and between 785f76b and a7d50cf.

📒 Files selected for processing (2)
  • packages/dart/lib/src/network/parse_query.dart
  • packages/dart/test/src/network/parse_query_test.dart

Comment thread packages/dart/lib/src/network/parse_query.dart Outdated
Encode string list elements by JSON-escaping then percent-encoding so quotes, backslashes and newlines remain valid in the server-decoded JSON, while characters like &, =, +, and # are protected from querystring parsing. Introduces _encodeStringElement (uses jsonEncode then Uri.encodeComponent) and updates _encodeStringElements to use it. Tests updated: stricter List<String>/List<int> typing, added a test to verify JSON-escaping of quotes and backslashes, and adjusted existing expectations for encoded delimiters.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 20, 2026
Copy link
Copy Markdown

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +321 to +331
/// JSON-escapes [value] and then percent-encodes the escaped content. The
/// JSON escaping keeps `"`, `\` and newlines valid inside the surrounding
/// string literal once the server URL-decodes the query; the percent
/// encoding keeps `&`, `=`, `+` and `#` from being chewed up by querystring
/// parsing on the way in.
String _encodeStringElement(String value) {
final String jsonString = jsonEncode(value);
return Uri.encodeComponent(
jsonString.substring(1, jsonString.length - 1),
);
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

The new _encodeStringElement correctly JSON-escapes before percent-encoding so values with ", \\, or newlines survive server URL-decoding as valid JSON. However, the scalar string constraints in this same class (e.g. whereEqualTo, whereNotEqualTo, whereStartsWith/EndsWith, whereContains) still call Uri.encodeComponent directly on the raw String; if the original value contains " or \\, server URL-decoding will reintroduce those characters unescaped inside the JSON and can make the where={...} value invalid JSON. Consider reusing this helper (or a shared encoder) for all String constraint builders to keep behavior consistent and avoid malformed-JSON queries for quoted/backslashed input.

Copilot uses AI. Check for mistakes.
Replace direct Uri.encodeComponent calls with a centralized _encodeStringElement in QueryBuilder string-handling methods (whereStartsWith, whereEqualTo, whereNotEqualTo, whereContains, and search) to ensure quotes and backslashes are JSON-escaped correctly when building queries. Add a unit test to verify percent-encoded sequences (%5C%22 and %5C%5C) appear in the built query and that raw `"`/`\` do not, preventing invalid JSON after server URL-decoding.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 20, 2026
@AdrianCurtin
Copy link
Copy Markdown
Author

Worth noting as a security upside of this change: the JSON-escape-then-percent-encode pipeline also closes a latent NoSQL-operator-injection path. Previously, a string value containing " was percent-encoded to %22, which the server URL-decodes back to a bare " inside the where={...} JSON literal — an attacker-controlled value could break out of the string and inject operators like $ne, $gt, $where, etc.

With _encodeStringElement, " is JSON-escaped to \" before percent-encoding, so the server sees a well-formed JSON string literal regardless of input. Same goes for \ and newlines.

Not the primary motivation for the fix, but a nice side effect.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 12.42%. Comparing base (785f76b) to head (da30001).

❗ There is a different number of reports uploaded between BASE (785f76b) and HEAD (da30001). Click for more details.

HEAD has 2 uploads less than BASE
Flag BASE (785f76b) HEAD (da30001)
4 2
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #1134       +/-   ##
===========================================
- Coverage   44.42%   12.42%   -32.01%     
===========================================
  Files          62        8       -54     
  Lines        3730      322     -3408     
===========================================
- Hits         1657       40     -1617     
+ Misses       2073      282     -1791     

☔ 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.

Simplify the Uri.encodeComponent call in _encodeStringElement to a single line and reformat test files for consistent indentation and argument/closure layout. These are whitespace/formatting changes only and do not alter runtime behavior or test logic.
Copy link
Copy Markdown

@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 (2)
packages/dart/lib/src/network/parse_query.dart (1)

339-348: regEx is the last where-method that still bypasses _encodeStringElement.

Every other where-method that takes a user-supplied String value (whereStartsWith, whereEndsWith, whereEqualTo/whereNotEqualTo, whereContainedIn/whereNotContainedIn, whereArrayContainsAll, whereContains, whereContainsWholeWord) was updated in this PR to flow through the new helper, but regEx(column, value) still inserts value directly into the JSON via _buildQueryWithColumnValueAndOperatorjsonEncode. JSON-encoding alone does not protect against the &/=/+/# querystring-splitting issue this PR is fixing, so a user calling regEx('field', 'a&b=c') will hit the same where=-parsing bug that whereContainedIn is being fixed for here.

It's fair to argue regex inputs are advanced usage, but for consistency with the rest of the encoding policy you may want to either route regEx through _encodeStringElement as well or document explicitly that callers of regEx must pre-encode. Flagging as recommended rather than blocking since (a) it's pre-existing behavior and (b) regex escaping has its own subtleties orthogonal to this fix.

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

In `@packages/dart/lib/src/network/parse_query.dart` around lines 339 - 348, The
regEx method currently inserts the raw user string into the JSON and should be
updated to use the same encoding helper as other where-methods; modify
regEx(String column, String value) so the value is passed through
_encodeStringElement before calling _buildQueryWithColumnValueAndOperator (use
the same encoding flow as whereStartsWith/whereEndsWith/whereContainedIn etc.),
ensuring special characters like &, =, +, # are safely encoded in the query JSON
rather than relying solely on jsonEncode.
packages/dart/test/src/network/parse_query_test.dart (1)

731-752: Optional: also assert end-to-end JSON roundtrip.

The current assertions verify presence/absence of substrings, which is sufficient for the immediate bug. As a nice-to-have, consider adding one assertion that mimics the server: extract the where= value, Uri.decodeComponent it, and jsonDecode the result — then check that the decoded $in list contains the original 'He said "hi"' and r'C:\path' strings verbatim. That would catch any future regression where the encoding produces something that "looks right" by substring but fails actual JSON parsing. Not required for this PR.

♻️ Sketch
final whereParam = Uri.parse('http://x?$queryString').queryParameters['where']!;
final decoded = jsonDecode(whereParam) as Map<String, dynamic>;
expect(decoded['topics']['\$in'], ['He said "hi"', r'C:\path']);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dart/test/src/network/parse_query_test.dart` around lines 731 - 752,
Add an end-to-end JSON roundtrip assertion in the test 'list-based where methods
JSON-escape quotes and backslashes in String elements': after building
queryString from QueryBuilder.name('Diet_Plans') with whereContainedIn('topics',
...) and calling buildQuery(), extract the where parameter (e.g. via
Uri.parse('http://x?$queryString').queryParameters['where']),
Uri.decodeComponent it, jsonDecode the resulting string into a Map, and assert
that decoded['topics']['\$in'] equals the original list ['He said "hi"',
r'C:\path'] to ensure the encoded value parses back to the exact original
strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/dart/lib/src/network/parse_query.dart`:
- Around line 339-348: The regEx method currently inserts the raw user string
into the JSON and should be updated to use the same encoding helper as other
where-methods; modify regEx(String column, String value) so the value is passed
through _encodeStringElement before calling
_buildQueryWithColumnValueAndOperator (use the same encoding flow as
whereStartsWith/whereEndsWith/whereContainedIn etc.), ensuring special
characters like &, =, +, # are safely encoded in the query JSON rather than
relying solely on jsonEncode.

In `@packages/dart/test/src/network/parse_query_test.dart`:
- Around line 731-752: Add an end-to-end JSON roundtrip assertion in the test
'list-based where methods JSON-escape quotes and backslashes in String
elements': after building queryString from QueryBuilder.name('Diet_Plans') with
whereContainedIn('topics', ...) and calling buildQuery(), extract the where
parameter (e.g. via
Uri.parse('http://x?$queryString').queryParameters['where']),
Uri.decodeComponent it, jsonDecode the resulting string into a Map, and assert
that decoded['topics']['\$in'] equals the original list ['He said "hi"',
r'C:\path'] to ensure the encoded value parses back to the exact original
strings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e063a41e-e4c6-47d4-b49e-32fd3549111c

📥 Commits

Reviewing files that changed from the base of the PR and between da30001 and 568fe98.

📒 Files selected for processing (2)
  • packages/dart/lib/src/network/parse_query.dart
  • packages/dart/test/src/network/parse_query_test.dart

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants