VNX-94: Code Injection
HTTP request data reaches eval, new Function, vm, string setTimeout, Python eval/exec/compile, PHP eval/assert/create_function/call_user_func, or Ruby eval/instance_eval/class_eval/module_eval, directly or through a variable.
| Field | Value |
|---|---|
| Rule ID | VNX-94 |
| Severity | Critical |
| SARIF level | error |
| Kind | sast |
| Languages | JavaScript / TypeScript, PHP, Python, Ruby |
| CWE | CWE-94 |
| CAPEC | CAPEC-242 |
| ATT&CK | T1059 |
| CWSS vector | CWSS:1.0/TI:C/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:C/DI:H/EX:H/EC:N/P:W |
| Tags | code-injection eval dynamic-execution cwe-94 |
| Source | internal/sast/rules/vnx-94.rego |
Overview
This rule reports a dynamic code evaluator whose argument carries HTTP request data: JavaScript eval(), new Function(), the vm module and string setTimeout/setInterval; Python eval(), exec() and compile(); PHP eval(), assert(), create_function() and a request-chosen call_user_func() callback; Ruby eval(), instance_eval(), class_eval() and module_eval() with a string. The request value may be used directly or through a variable assigned or destructured from it (every name const { a, b: c } = req.body binds), or from such a variable, in the 25 lines above; Django request.COOKIES counts as request data. Evaluators over constants, values passed through a numeric conversion or ast.literal_eval, function arguments to setTimeout, and Ruby block forms are not reported. Evaluation of non-constant code with no visible request source is reported by the language rules (VNX-NODE-002, VNX-PY-002), which leave the lines this rule reports to it; VNX-913 used to report the Python and JavaScript subset of these lines and now defers to this rule; and preg_replace with /e by VNX-PHP-009. Code injection allows an attacker to supply arbitrary code that is executed by the application with the full privileges of the runtime process. This maps to CWE-94: Improper Control of Generation of Code (‘Code Injection’).
Why This Matters
Code injection is the most direct and complete form of application compromise possible: the attacker writes the code, the server runs it. Unlike SQL injection or command injection, there is no database or OS boundary, the attacker’s payload executes in the same language, the same process, and with the same permissions as the application. In JavaScript, eval(userInput) gives the attacker access to the entire Node.js environment including require('fs') and require('child_process'). In Python, eval(user_input) can call __import__('os').system('...'). The rule also flags setTimeout(stringArg) and setInterval(stringArg) in JavaScript because these functions evaluate their first argument as code when it is a string rather than a function reference.
What Gets Flagged
// FLAGGED: request data evaluated directly, or through a variable
const result = eval(req.query.expr);
const body = req.body.rule;
const fn = new Function('item', body);
setTimeout("refresh('" + req.query.panel + "')", 500);
# FLAGGED
return str(eval(request.args.get("q")))
code = request.form["code"]
exec(code, {"__builtins__": {}})
<?php
// FLAGGED
$expr = $_GET['expr'];
eval('$result = ' . $expr . ';');
call_user_func($_GET['callback'], $data);
# FLAGGED
render plain: eval(params[:expr]).to_s
Widget.class_eval(request.body.read)
Not flagged: eval('1 + 2'), ast.literal_eval(request.args["v"]), setTimeout(() => refresh(req.query.panel), 500), eval('$r = ' . intval($_GET['n']) . ';'), config.instance_eval do ... end.
Remediation
- Do not use eval or equivalent functions with external data. In almost all cases there is a safer alternative.
# SAFE: Use ast.literal_eval for parsing data expressions (not code)
import ast
def parse_value(s: str):
# Only parses Python literals: strings, numbers, lists, dicts, tuples, bools, None
return ast.literal_eval(s)
# SAFE: For mathematical expressions, use a dedicated expression parser
# e.g. simpleeval, numexpr, or operator trees, never eval()
// SAFE: Use Function properly, only with static, developer-controlled template strings
// Better: use a math expression library like mathjs
const { evaluate } = require('mathjs');
app.post('/calculate', (req, res) => {
try {
const result = evaluate(req.body.expression); // sandboxed math evaluation
res.json({ result });
} catch (e) {
res.status(400).json({ error: 'Invalid expression' });
}
});
// SAFE: setTimeout with a function reference, not a string
setTimeout(() => cleanup(userId), 1000);
<?php
// SAFE: Use a whitelist-based expression evaluator rather than eval
// Or restructure logic to avoid dynamic code execution entirely
// If dynamic dispatch is needed, use a dispatch table:
$allowed = ['add' => fn($a,$b) => $a + $b, 'mul' => fn($a,$b) => $a * $b];
$op = $_POST['operation'];
if (!array_key_exists($op, $allowed)) {
http_response_code(400); exit;
}
$result = $allowed[$op]($a, $b);
-
Replace
preg_replacewith/emodifier withpreg_replace_callback(). The/emodifier was deprecated in PHP 5.5 and removed in PHP 7.0. -
Use a sandboxed execution environment if dynamic code execution is a genuine requirement. Node.js has
vm.runInNewContext(), Python hasRestrictedPython, and there are purpose-built sandboxes for all major languages. -
Audit all existing uses of eval/exec even when the argument appears to be developer-controlled, code paths change over time and a future refactor may introduce a vulnerability.
References
- CWE-94: Code Injection
- OWASP Code Injection
- OWASP Injection Prevention Cheat Sheet
- Python ast.literal_eval documentation
- CAPEC-242: Code Injection
- MITRE ATT&CK T1059 – Command and Scripting Interpreter
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-94
// nosec vnx-94
Or record an ignore rule: vulnetix ignore add --rule vnx-94 --reason "...". See Suppressing findings.