Query string injection

The query_string and simple_query_string queries parse a mini-language (Lucene query syntax) directly from the supplied text. When an application drops a user's search term into that query without escaping the reserved characters, the term stops being data and becomes query logic. Unlike SQL, there is no out-of-band interpreter to subvert: the parser itself grants field selection, boolean combination, wildcards, ranges, and existence tests.

Scope. For authorized penetration tests, CTF labs, and code review of systems you own or are contracted to assess.

Vulnerable pattern

POST /products/_search
{
  "query": {
    "query_string": { "query": "USER_TERM" }
  }
}

The application substitutes the raw search box value into USER_TERM. Reserved characters (+ - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /) and the keywords AND, OR, NOT, TO are all live.

Boolean operators

Default field searches can be combined or negated. If the app expects a plain keyword, supplying operators rewrites the intent:

laptop OR price:[0 TO 999999]
laptop AND NOT discontinued:true

OR with a broad second clause pulls in documents the original term would never match.

Field targeting

field:value jumps to any indexed field, including ones the UI never exposes. An attacker who knows or guesses the mapping reads fields outside the search box's scope:

name:laptop OR is_internal:true
* OR api_key:*
owner_id:12 OR owner_id:13

A leading * or empty term combined with OR on a sensitive field turns a product search into a dump of documents carrying that field.

Wildcards and ranges

Wildcards (*, ?) and ranges ([min TO max], {exclusive}) widen matches and enable value probing:

email:*@corp.example
created:[2020-01-01 TO 2026-12-31]
price:<0 OR amount:>999999

Leading wildcards (*term) are rejected by default but often re-enabled with allow_leading_wildcard; where allowed, secret:* confirms any document that has the field.

Widening with _exists_

The _exists_:field token matches every document where a field is present, regardless of value, a fast way to break out of a filtered view and enumerate which documents carry privileged attributes:

_exists_:password_reset_token
name:anything OR _exists_:ssn

Bypassing an appended filter

Applications often concatenate a trusting term with a trailing scope filter:

{ "query": { "query_string": { "query": "USER_TERM AND tenant:acme" } } }

A term of x OR _exists_:id yields x OR _exists_:id AND tenant:acme. Because AND binds tighter than OR, the _exists_:id arm matches every document independent of the tenant clause, escaping tenant isolation. Forcing grouping makes the bypass explicit where the injection point allows parentheses:

x) OR (_exists_:id

This closes the intended group early and opens an unscoped one, discarding the appended filter entirely.

Analyzer and boosting tells

Fuzzy (term~), proximity ("a b"~5), and boost (term^10) operators confirm the parser is live when reconnaissance is needed: a response that treats test~2 differently from the literal string test~2 proves query_string parsing rather than a match query.

References

Cookie Consent

We use cookies to enhance your experience. Learn more