Remote Code ExecutionEval Injection

Eval Injection: When a Runtime Treats User Text as Code

Dynamic evaluators such as eval, new Function and exec turn text into program logic. Learn how the boundary fails, how harmless arithmetic proves it, and how a narrow data model replaces evaluation.

SelfSec Team5 min read
Part 2 of 2from theRemote Code Executionseries
6*7===42

At a glance

CWE-95
Interpreter
Application language runtime
Required condition
Untrusted text reaches eval, exec, Function, or an equivalent dynamic evaluator.
Potential impact
Arbitrary application-level code can run with the service process's authority.
Primary defense
Parse an explicit data format or interpret a small allow-listed grammar instead.

Safe practice: use benign proof only, in a disposable local lab or on a system you are explicitly authorized to test.

On this page

Eval injection is code execution through the language's own evaluator. JavaScript has direct eval, new Function and legacy string-taking timer forms in browsers; Python has eval and exec; Ruby and PHP expose their own evaluators. Some older PHP examples also mention string assertions and create_function, but those behaviors are deprecated or removed in current PHP versions. Review the runtime actually deployed rather than copying a historical sink list.

This is specifically about in-language code evaluation: data crossing into the interpreter that is already executing your app. It is a sibling of OS-command injection and server-side template injection — those reach code execution too, but through a system shell and a template engine respectively, and each has its own dedicated post; here we stay narrowly on the language-level eval sinks. This post shows how input reaches one, what it costs, and why the only reliable fix is to never evaluate untrusted input at all.

When this technique applies

User influence must reach source text passed to an evaluator. Dynamic property access, a safe JSON parser and JavaScript import() with a controlled module specifier have different risk models and should not automatically be labeled eval injection. The defining behavior is that attacker-controlled characters become language grammar.

Direct eval executes in its surrounding scope, while new Function has different scope behavior. Neither distinction creates a safe boundary for untrusted input; it only changes which variables and capabilities are immediately reachable.

How it happens

The pattern appears wherever a developer wants to turn a string into behaviour and reaches for the quickest tool. A rules or filtering feature that lets users supply a small expression is a classic case:

app.get('/filter', (req, res) => {
  const rule = req.query.rule;
  const items = loadItems();
  const matches = items.filter(item => eval(rule));
  res.json(matches);
});

The intent is that rule is something like item.price < 50. But eval does not parse expressions in a sandbox — it runs whatever string it is given with the full authority of the surrounding code, with require, the filesystem and the process all in reach. The same shape recurs across the family: new Function("return " + input), setTimeout(userString, 0), Python eval(request.args["expr"]) or exec(payload), Ruby eval(params[:code]), PHP eval($_GET['x']) and create_function('', $_POST['body']). In each, a value that should be inert data is handed to an API whose entire job is to execute it.

A safe proof with arithmetic

In a disposable lab, send a fixed expression whose result is easy to distinguish from literal reflection:

GET /filter?rule=6*7===42 HTTP/1.1
Host: app.example

eval runs the expression for each item. Because it evaluates to true, the response contains the normal seeded list:

["lab-item-a", "lab-item-b"]

Pair it with 6*7===41, which returns no items. A stable true/false difference proves that the parameter was interpreted as JavaScript rather than as a literal filter value. That harmless proof is enough: because the sink accepts general language syntax, the reachable impact must be assessed from the runtime context without executing operating-system commands or reading secrets.

The bug is not that the language can evaluate strings. It is that a value the user supplied was allowed to be one of those strings — erasing the line between what the request asked for and what the program does.

What an attacker gains

Once attacker input executes in the interpreter, it inherits everything that process holds:

  • Full code execution in the application's runtime, with its modules, filesystem access and network reach.
  • Credential theft from environment variables, config and connection strings the process can read.
  • Pivot to the host and beyond — from in-language execution it is a short step to spawning OS commands and moving laterally.
  • Data tampering and destruction across whatever the service can touch.

Eval injection is commonly critical because it starts with application-level code execution. The exact severity still depends on authentication requirements, reachable capabilities and process isolation.

The fix: never evaluate input, parse it safely instead

The reliable defence is to keep untrusted input out of every evaluation sink: data must never be parsed as code. Most uses of eval are trying to parse data or implement a small rules language. Model those needs explicitly. For a product filter, accept a fixed schema and interpret only known fields and operations:

const operations = {
  lt: (left, right) => left < right,
  eq: (left, right) => left === right
};

function matches(item, rule) {
  if (!['price', 'category'].includes(rule.field)) {
    throw new Error('Invalid field');
  }
  if (!Object.hasOwn(operations, rule.operation)) {
    throw new Error('Invalid operation');
  }
  return operations[rule.operation](item[rule.field], rule.value);
}

The request can now express “price less than 50” as data, but cannot name a function or write JavaScript syntax. For Python literal data, ast.literal_eval is narrower than eval, while JSON with schema validation is usually clearer across system boundaries. When genuinely offering user-authored logic, design a small grammar and interpreter or place the feature behind a deliberately maintained isolation boundary; do not present a general runtime sandbox as a simple input-validation fix.

Fixes that do not hold

  • Removing one function name: general language syntax offers aliases and other capabilities.
  • A regular expression that permits “math”: parsing a programming language with a blocklist is brittle.
  • Encoding quotes: many valid expressions need no quote, and decoding may happen before evaluation.
  • Hiding the result: timing, errors or later state can still reveal execution.
  • Using Node's vm as a security boundary: Node's own documentation states that it is not a security mechanism.

A developer review checklist

  1. Search for direct evaluators and wrappers around them.
  2. Identify whether request, file, message or stored text can reach the source string.
  3. Replace data parsing with JSON and an explicit schema.
  4. Replace rule evaluation with fixed fields, operations and typed values.
  5. Remove unused evaluator features and runtime capabilities.
  6. Prove fixes with true/false arithmetic and confirm the input is now treated literally or rejected.

How SelfSec finds it

SelfSec probes language-level evaluation sinks with benign expressions whose computed results are unambiguous, then uses response and timing differentials to distinguish execution from literal reflection. Confirmed findings include a reproduction request. Core scan processing runs locally on 127.0.0.1.

References

SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.

Do both things about Remote Code Execution

SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.

The Remote Code Execution series

  1. 01OverviewRemote Code Execution: When User Input Becomes Server Code6 min
  2. 02Eval InjectionEval Injection: When a Runtime Treats User Text as CodeReading

Related reading