Choose your stack

Stacks without a card

How it happens

  • A string from the request is passed to a code evaluator: eval, new Function or the vm module in Node.js, SpEL with a StandardEvaluationContext in Spring, or CSharpScript in .NET. The "formula" is a program with the server's reach.
  • A sandbox is assumed where none exists. Node's vm module is documented as not a security mechanism; a host object placed in the context exposes the host Function constructor through its prototype chain, and the timeout stops nothing.
  • A denylist stands in for a grammar. Blocking words such as process, Runtime or T( is defeated by string concatenation, bracket access, reflection or alternate gadgets; the allowed input must be defined by what you accept, not by what you reject.
  • An expression language is evaluated with full power. SpEL's StandardEvaluationContext allows type references (T(java.lang.Runtime)), bean references and constructor calls, so an expression built from input reaches the whole runtime.
  • The feature really only needs arithmetic or a short set of named operations, but it was built on a general evaluator for convenience.

Fix principles

  • Do not turn request input into code. Decide the small grammar the feature needs (here numbers, the allowlisted field names, the four arithmetic operators and parentheses) and parse exactly that.
  • Evaluate the parsed form over the data: an identifier resolves to a field value, never to a variable, function or global. A short recursive-descent parser, or a maintained expression library whose evaluator is data-only, both work; the control is the grammar.
  • Where an expression language is unavoidable, use its data-only mode: SpEL's SimpleEvaluationContext.forReadOnlyDataBinding() allows property navigation and operators but no type references, bean references or constructor calls.
  • Reject anything outside the grammar with a clear client error, never a fallback to eval and never a 500. Keep the result typed and bounded.
  • Treat Node's vm, a fresh ScriptOptions, or a cleared SpEL context as conveniences, not sandboxes: none safely runs untrusted code. Use real isolation (a separate process with an OS sandbox, or a dedicated service) only when you must run user-authored code.

The scenario

A dashboard lets a user define a pricing formula over a row of numbers (price, quantity, discount). POST /quote evaluates the formula and returns a number. The fixed build parses a fixed arithmetic grammar (or binds the row into a data-only evaluation context); the vulnerable build runs the formula as code and leaks a secret the test places in the environment; the wrong build tries to sandbox the code and is escaped.

How the cards prove it

  • Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong implementations must fail. run-all.mjs does this for every card.
  • The exploit tests send formulas that read an environment secret, reach a host object through its prototype chain, or invoke a process. The secret is a number, so if it reaches the response, code ran. A safe evaluator rejects these; the functional tests keep ordinary arithmetic working.
  • On your own endpoint, send the same payloads with a known secret in the environment and confirm it never appears in a response, and that a process is never started.
  • Static analysis helps but is not sufficient: Semgrep flags eval but not new Function or vm; its SpEL rule flags every evaluation site including the safe SimpleEvaluationContext; CodeQL models the sinks. Each card's scannerRules records what each tool found.

Identifiers

CWE-94 (Improper Control of Generation of Code) is the weakness the cards are named after: a request string is turned into code and run. CWE-95 (Eval Injection), its child, is the specific case these cards show, where the string is evaluated by eval, new Function, a scripting engine or an expression language. Both are in OWASP Top 10:2025 A05 Injection (whose official CWE list includes CWE-94 and CWE-95). CAPEC-242 (Code Injection) and CAPEC-35 (Leverage Executable Code in Non-Executable Files) are attack patterns MITRE links to CWE-94. The 2025 CWE Top 25 ranks CWE-94 tenth. This is distinct from server-side template injection (see the template-injection cards): there the sink is a template engine, here it is a general code or expression evaluator.

Attack patterns:

References

  1. OWASP Top 10:2025 A05 Injection
  2. CWE-94: Improper Control of Generation of Code ('Code Injection')
  3. CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')
  4. OWASP Code Injection
  5. Node.js VM: "The vm module is not a security mechanism"
  6. Spring Framework: SpEL EvaluationContext (SimpleEvaluationContext and StandardEvaluationContext)

Practice in the browser

Guides on this weakness

Guide

Keep untrusted values out of executable syntax

Choose a safe interface for each interpreter and bound the work performed by parsers and converters.

Guide

Keep untrusted input out of eval, expression languages and templates

Keep code and templates constant, pass input as data, and treat in-process sandboxes as one layer, not a boundary.

Guide

Keep untrusted CI work outside the release boundary

Review workflow authority, persistent state and artifact handoffs before contributor-controlled code reaches privileged jobs.

Guide

Prevent DOM XSS, postMessage abuse and client-side injection

Trace attacker-controlled values from the URL, messages and storage to the browser APIs that parse HTML or run code, then close those sinks with safe APIs, a sanitizer, Trusted Types and strict message checks.

Guide

Harden GitHub Actions against workflow and supply-chain attacks

Close the paths that give untrusted pull-request code secrets or a write token: pass context through env, keep pull_request_target away from fork code, pin actions to commit SHAs, scope GITHUB_TOKEN and OIDC, and treat artifacts as data.

Guide

Prevent file inclusion: LFI, RFI, dynamic imports and template names

Stop request data from choosing which file, module or template your application loads: local and remote file inclusion in PHP, dynamic require() and import() in Node.js, importlib and template names in Python, Thymeleaf view names in Spring, render in Rails, and the allowlist fix that works in all of them.

Guide

Sandbox model-generated code and computer-use agents

Run the code an agent writes, and agents that drive a browser or desktop, inside a boundary built for hostile code: a disposable runtime with no secrets and no default network, hard resource limits, and a person in front of every side effect.

Case study

Case study: ingress-nginx CVE-2025-1974 and the exposed admission webhook (2025)

How an admission webhook open to the whole Pod network made ingress-nginx's configuration-injection bugs a 9.8 cluster-takeover risk in 2025, and what its 2026 retirement means.

Case study

Case study: Spring4Shell (CVE-2022-22965)

How Spring's data binding let request parameters reach the class loader on JDK 9 and later in 2022, why the denylist workaround leaked, and what it teaches about framework-level mass assignment.