Problem
Like::GetSqlValue (src/query_predicates.cc:97) builds the comparison value as %value% but does not escape % or _ characters contained in the value itself, and the generated LIKE clause (QueryPredicate::Evaluate) has no ESCAPE clause.
Impact
If the user-supplied value contains % or _, those characters act as LIKE wildcards rather than literals, so the match semantics differ from what the caller intended (e.g. searching for 50% matches far more than the literal text). The value is still bound as a parameter, so this is a correctness/robustness problem rather than injection.
Suggested direction
Escape %, _ (and the escape character) in the bound value and emit a corresponding ESCAPE '\' clause, so the user's text is matched literally.
Implementation note — the ESCAPE clause must survive Clone()
The value escaping and the ESCAPE clause are a pair: SQLite's LIKE has no default escape character, so escaping the bound value only works if the SQL also declares ESCAPE '\'. Naively overriding Like::Evaluate() to append the clause is not sufficient, because of how predicates are cloned:
QueryPredicate::Clone() (src/query_predicates.cc:34) returns new QueryPredicate(symbol_, member_name_, value_) — a base QueryPredicate, not a Like. Like does not override Clone().
BinaryPredicate (used by And / Or) stores left.Clone() / right.Clone(), so a Like combined with And/Or is downgraded to a base QueryPredicate before Evaluate() runs. A Like-only Evaluate() override would therefore emit ... LIKE ? (no ESCAPE) for every compound predicate, while the bound value is still escaped — i.e. broken matching.
Make the ESCAPE clause travel with the cloned state. Simplest options:
- have
QueryPredicate::Evaluate() append ESCAPE '\' when symbol_ == "LIKE" (both symbol_ and the escaped value_ are copied by Clone()), or
- give
QueryPredicate an optional trailing-clause/suffix field that Clone() copies and Like sets.
Either way, add a test that a Like inside And/Or still matches literally, to lock in the clone-safe behavior.
Problem
Like::GetSqlValue(src/query_predicates.cc:97) builds the comparison value as%value%but does not escape%or_characters contained in the value itself, and the generatedLIKEclause (QueryPredicate::Evaluate) has noESCAPEclause.Impact
If the user-supplied value contains
%or_, those characters act as LIKE wildcards rather than literals, so the match semantics differ from what the caller intended (e.g. searching for50%matches far more than the literal text). The value is still bound as a parameter, so this is a correctness/robustness problem rather than injection.Suggested direction
Escape
%,_(and the escape character) in the bound value and emit a correspondingESCAPE '\'clause, so the user's text is matched literally.Implementation note — the ESCAPE clause must survive
Clone()The value escaping and the
ESCAPEclause are a pair: SQLite'sLIKEhas no default escape character, so escaping the bound value only works if the SQL also declaresESCAPE '\'. Naively overridingLike::Evaluate()to append the clause is not sufficient, because of how predicates are cloned:QueryPredicate::Clone()(src/query_predicates.cc:34) returnsnew QueryPredicate(symbol_, member_name_, value_)— a baseQueryPredicate, not aLike.Likedoes not overrideClone().BinaryPredicate(used byAnd/Or) storesleft.Clone()/right.Clone(), so aLikecombined withAnd/Oris downgraded to a baseQueryPredicatebeforeEvaluate()runs. ALike-onlyEvaluate()override would therefore emit... LIKE ?(noESCAPE) for every compound predicate, while the bound value is still escaped — i.e. broken matching.Make the
ESCAPEclause travel with the cloned state. Simplest options:QueryPredicate::Evaluate()appendESCAPE '\'whensymbol_ == "LIKE"(bothsymbol_and the escapedvalue_are copied byClone()), orQueryPredicatean optional trailing-clause/suffix field thatClone()copies andLikesets.Either way, add a test that a
LikeinsideAnd/Orstill matches literally, to lock in the clone-safe behavior.