Choose your stack
Fix code injection in Node.js and Express
Do not turn a request string into code. eval, new Function and node:vm all run it, and node:vm is not a sandbox.
Open the fix card Python · FlaskFix code injection in Python and Flask
Never evaluate user input as code. Parse the expression with ast.parse and walk the tree, allowing only numeric literals, the supplied variables and arithmetic operators; refuse everything else. eval(expr, {"__builtins__": {}}, vars) is not a sandbox: attribute access still reaches any object.
Open the fix card Java · Spring BootFix code injection in Java and Spring Boot
Do not evaluate request input as SpEL with a StandardEvaluationContext: it allows T(...) type references and method calls, so the formula runs code.
Open the fix card C# · ASP.NET CoreFix code injection in C# and ASP.NET Core
Do not run request input as code. Roslyn's CSharpScript executes the formula with the server's privileges, and a denylist of dangerous APIs cannot make it safe: it misses Environment.GetEnvironmentVariable and is never complete.
Open the fix card Python · FastAPIFix code injection in Python and FastAPI
Never evaluate user input as code. Parse the expression with ast.parse and walk the tree, allowing only numeric literals, the supplied variables and arithmetic operators; refuse everything else.
Open the fix cardStacks without a card
- TypeScript and Next.js: no card yet. The nearest is Fix code injection in Node.js and Express, in the same language.
- Python and Django: no card yet. The nearest is Fix code injection in Python and Flask, in the same language.
- Go and net/http: no card yet. The nearest is Fix code injection in Node.js and Express.
- Ruby on Rails: no card yet. The nearest is Fix code injection in Node.js and Express.
- PHP and Laravel: no card yet. The nearest is Fix code injection in Node.js and Express.
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
- OWASP Top 10:2025 A05 Injection
- CWE-94: Improper Control of Generation of Code ('Code Injection')
- CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')
- OWASP Code Injection
- Node.js VM: "The vm module is not a security mechanism"
- Spring Framework: SpEL EvaluationContext (SimpleEvaluationContext and StandardEvaluationContext)
Practice in the browser
- Actions injection lab: Flag GitHub Actions expression injection · Practitioner, JavaScript
Guides on this weakness
Keep untrusted values out of executable syntax
Choose a safe interface for each interpreter and bound the work performed by parsers and converters.
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.
Keep untrusted CI work outside the release boundary
Review workflow authority, persistent state and artifact handoffs before contributor-controlled code reaches privileged jobs.
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.
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.
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.
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: 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: 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.