Query string injection

When a search term flows unescaped into query_string or simple_query_string, Lucene operators let an attacker retarget fields, flip boolean logic, and widen result sets past intended filters.

3 min di letturaaggiornato il 7 ott 2026florianamette1 contributore
Chiedi al tuo assistente (MCP)Modifica su GitHub

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.

Vulnerable pattern#

json
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:

code
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:

code
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:

code
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:

code
_exists_:password_reset_token
name:anything OR _exists_:ssn

Bypassing an appended filter#

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

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

A term of x OR _exists_:id yields x OR _exists_:id AND tenant:acme. Do not rely on boolean precedence here: Lucene's classic query parser does not apply standard AND-over-OR precedence, it compiles to a flat BooleanQuery of MUST/SHOULD clauses, and a trailing AND tenant:acme makes tenant:acme a required (MUST) clause, so this form does not reliably drop the tenant scope. The reliable bypass is to force grouping where the injection point allows parentheses, discarding the appended clause outright:

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

Tools#

  • curl: run query_string searches with injected Lucene operators.
  • Burp Repeater: deliver field-targeting and grouping payloads through the search parameter.

References#