Skip to content

Fix LIKE operator to escape wildcards in user-supplied values#33

Merged
jkalias merged 1 commit into
mainfrom
claude/escape-like-wildcards
Jul 7, 2026
Merged

Fix LIKE operator to escape wildcards in user-supplied values#33
jkalias merged 1 commit into
mainfrom
claude/escape-like-wildcards

Conversation

@jkalias

@jkalias jkalias commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Fixed the Like query predicate to properly escape SQLite wildcard characters (%, _, and \) in user-supplied search values, ensuring they are matched literally rather than interpreted as wildcards. This prevents unintended pattern matching behavior and potential SQL injection vulnerabilities.

Key Changes

  • Added EscapeLikeWildcards() helper function in query_predicates.cc that escapes backslash, percent, and underscore characters by prefixing them with a backslash
  • Updated QueryPredicate::Evaluate() to emit an ESCAPE '\' clause for all LIKE operations, enabling the escape sequences to take effect in SQLite
  • Modified Like::GetSqlValue() to apply EscapeLikeWildcards() to all user-supplied values (text, int, bool, real) before wrapping them with the outer "contains" wildcards (%)
  • Added comprehensive test coverage including:
    • Literal % matching in search values
    • Literal _ matching in search values
    • Literal \ matching in search values
    • LIKE combined with AND predicates (verifying escape clause survives Clone())
    • Regression test ensuring non-wildcard values still work as substring matches

Implementation Details

  • The escape clause is placed in QueryPredicate::Evaluate() (not a Like-specific override) to ensure it survives the Clone() operation used by BinaryPredicate when combining conditions with And()/Or()
  • Backslashes are escaped as they are encountered during the escaping process, preventing double-escaping of the escape character itself
  • The solution maintains backward compatibility for values without special characters while fixing the security and correctness issues with wildcard characters

https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK

Like::GetSqlValue wrapped the bound value in %...% but never escaped %, _,
or \ occurring inside the caller's value itself, and the generated LIKE
clause had no ESCAPE clause. SQLite's LIKE has no default escape character,
so a value like "50%" matched far more than the literal text - not
injection (the value is still bound as a parameter), but wrong match
semantics.

Escape \, % and _ (backslash first, single pass, so an inserted escape
backslash is never re-escaped) in Like::GetSqlValue before wrapping in the
outer "contains" percents, and emit " ESCAPE '\\'" from
QueryPredicate::Evaluate() whenever symbol_ == "LIKE".

The ESCAPE clause has to live in QueryPredicate::Evaluate() rather than a
Like-only override: QueryPredicate::Clone() returns a base QueryPredicate
(not a Like), and BinaryPredicate (And/Or) stores Clone()d operands, so a
Like combined via And()/Or() is downgraded to a base QueryPredicate before
Evaluate() runs. Since symbol_ and the already-escaped value_ both survive
Clone(), keying the ESCAPE clause off symbol_ keeps compound predicates
correct too - confirmed with a dedicated Clone-survival test.

Tests: literal %, literal _, literal backslash, a Like combined via And()
still matching literally (locks in the Clone-survival fix), and a
regression test that a plain Like with no special characters still behaves
as a substring/contains match. Updated the pre-existing
LikePayloadStaysInBindings test's expected SQL/binding strings to reflect
the now-correct escaped output. Confirmed 6 of 8 new/updated Like assertions
fail on the pre-fix code and all pass after. Full suite: 72/72 tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK
@jkalias jkalias merged commit 08dcf6a into main Jul 7, 2026
15 checks passed
@jkalias jkalias deleted the claude/escape-like-wildcards branch July 7, 2026 19:00
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