Skip to content

fix(W-23622931): escape single quotes in SmartSQL json_extract path and FTS MATCH value - #2993

Open
wmathurin wants to merge 2 commits into
forcedotcom:devfrom
wmathurin:W-23622931-smartsql-injection-escaping
Open

fix(W-23622931): escape single quotes in SmartSQL json_extract path and FTS MATCH value#2993
wmathurin wants to merge 2 commits into
forcedotcom:devfrom
wmathurin:W-23622931-smartsql-injection-escaping

Conversation

@wmathurin

Copy link
Copy Markdown
Contributor

Summary

  • Sink 1 — json_extract path (SmartSqlHelper.java:176): escapes ''' in the soup path before building json_extract(soup, '$.path'). A quote in the path broke out of the SQL string literal, allowing SmartSQL injection via a non-indexed query path (CWE-89).
  • Sink 2 — FTS MATCH value (QuerySpec.java:384): escapes ''' in the qualified match key before inlining into MATCH 'value'. Statement arg binding doesn't work for FTS MATCH so inlining is unavoidable; escaping prevents breaking out of the surrounding string literal.
  • Both sinks are reachable from the SmartStore Cordova bridge in hybrid apps (W-23622931).

Test plan

  • Build SmartStore: ./gradlew :libs:SmartStore:build — confirmed passing
  • Run SmartStore tests: ./gradlew :libs:SmartStore:connectedAndroidTest — confirmed all passing

…nd FTS MATCH value

Escape single quotes in two injection sinks in SmartStore:
1. SmartSqlHelper: unindexed path used in json_extract(soup, '$.path')
   - a quote in the path broke out of the SQL string literal
2. QuerySpec: qualifyMatchKey result inlined into MATCH 'value'
   - statement arg binding doesn't work for FTS MATCH, so inlining is required;
     escaping '' prevents breaking out of the surrounding string literal

Both sinks are reachable from the SmartStore Cordova bridge in hybrid apps.
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
2 Warnings
⚠️ libs/SmartStore/src/com/salesforce/androidsdk/smartstore/store/QuerySpec.java#L115 - The indentation string here is different from on the previous line (" " vs \t)
⚠️ libs/SmartStore/src/com/salesforce/androidsdk/smartstore/store/QuerySpec.java#L411 - Implicitly using the default locale is a common source of bugs: Use toLowerCase(Locale) instead. For strings meant to be internal use Locale.ROOT, otherwise Locale.getDefault().

Generated by 🚫 Danger

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for tightening these SmartSQL sinks, Wolfgang. I traced the query-construction path and the fix is correct on both sinks it targets:

  • SmartSqlHelper.java:176 (non-indexed json_extract path) — every {soup:path} reference in smart/exact/like/range/order queries routes through convertSmartSql into this line, so escaping here covers those callers. Inside the '$.<path>' string literal, ' is the only metacharacter and '' is the correct SQLite escape, so the break-out is closed. (The $\$ handling at line 149 is orthogonal regex-replacement escaping and is unaffected.)
  • QuerySpec.java:384 (FTS MATCH value) — this is the one query type that inlines its key rather than binding a ? arg (getArgs() returns null for match; exact/like/range bind params and are safe). Escaping the output of qualifyMatchKey(...) — i.e. after the field: qualifiers are inserted — is the right layer; escaping earlier would let the qualifier regex re-tokenize the doubled quotes.

I confirmed platform parity: the iOS twin (SalesforceMobileSDK-iOS PR 4132, W-23622931, merged to dev on 2026-08-14) makes the identical change at the same two sinks and the same layer, so this PR is consistent with the shipped iOS fix.

Requested change — fold in the third json_extract sink (SmartStore.java:379)

A third, structurally identical sink is left unescaped in SmartStore.java:379 (index-registration path):

columnName = "json_extract(" + SOUP_COL + ", '$." + indexSpec.path + "')";

Here indexSpec.path is concatenated unescaped. It's reachable from the same hybrid attack surface: JS → SmartStorePlugin pgRegisterSoupIndexSpec.fromJSON (which does no validation on path, IndexSpec.java:44) → this DDL, where the column expression is baked into CREATE TABLE/CREATE INDEX and persisted in soup_index_map. A ' in the index path breaks out of the literal the same way — same CWE-89 class, just at registration time instead of query time. Since it's the identical fix in the same file, could we fold it into this PR so the sink family is fully closed?

columnName = "json_extract(" + SOUP_COL + ", '$." + indexSpec.path.replace("'", "''") + "')";

I confirmed this line isn't being addressed in any other open/merged PR or GUS item. For cross-platform parity, note the iOS twin (PR 4132) fixed the two query-time sinks but left the equivalent SFSmartStore.m:1120 index-path sink untouched, so a matching iOS follow-up would keep the platforms aligned.

Non-blocking: No regression test accompanies the escaping — see the inline note on SmartSqlHelper.java:176.

Reviewed at HEAD 7e31f066.

This review was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.

Add testConvertSmartSqlForNonIndexedColumnWithSingleQuoteInPath to SmartSqlTest
to verify that a ' in a non-indexed path is doubled in the json_extract literal,
and testMatchQuerySmartSqlWithSingleQuoteInMatchKey to QuerySpecTest to verify the
same doubling in the FTS MATCH predicate (QuerySpec.java:384).
@JohnsonEricAtSalesforce

Copy link
Copy Markdown
Contributor

The regression tests address the non-blocking note — thanks. The one requested change is still open: the third json_extract sink at SmartStore.java:379 (indexSpec.path, index-registration path) is unescaped and reachable from the same hybrid bridge via pgRegisterSoupIndexSpec.fromJSON (no path validation). It's the same CWE-89 construction you've already closed at the two query-time sinks, so folding in the one-liner there —

columnName = "json_extract(" + SOUP_COL + ", '$." + indexSpec.path.replace("'", "''") + "')";

— would close the sink family completely rather than leaving a registration-time sibling. (I confirmed no other open/merged PR or work item covers this line. The iOS twin PR left the equivalent SFSmartStore.m:1120 sink untouched too, so a matching iOS follow-up would keep the platforms in parity.) Keeping the review as CHANGES_REQUESTED on that item.

This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.

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