Skip to content

4.5 Review Code Injection

4.5 - Review Code Injection

Code injection appears when attacker-controlled input is passed to an evaluator that executes it as code. Start from rule engines, formula fields, admin consoles, and plugin systems. Trace each user-supplied string to eval, script engines, or dynamic template compilation.

What This Vulnerability Is

Code injection is similar to command injection, but the attacker injects source code in a language the application executes—JavaScript, Python, Groovy, SpEL, OGNL—not OS shell commands. The application exposes an evaluation primitive for business rules, math expressions, or user-defined logic. Attacker input becomes part of that program and runs with the interpreter's privileges.

In command injection, the attacker extends existing OS command execution. In code injection, the attacker supplies program text the runtime compiles or interprets. This relates to CWE-94 (Improper Control of Generation of Code) and CWE-95 (Improper Neutralization of Directives in Dynamically Evaluated Code).

Vulnerability Characteristics (Where to Identify Them)

Signal Where to look
Feature type Formula fields, workflow rules, admin script consoles, plugin hooks, webhook transformers
Input entry HTTP parameters, JSON rule bodies, uploaded config, admin-only text areas
Evaluation APIs eval, exec, ScriptEngine.eval, SpEL, OGNL, Function(), pickle/yaml unsafe loaders
Concatenation patterns Expression strings built with + or f-strings before evaluation
Weak controls Sandbox claims without verified restrictions on imports, reflection, file I/O
Overlap with SSTI Template engines that compile user-supplied source at runtime

Attack Payloads

Use these in authorized tests when user input reaches an evaluator. Replace TARGET with the vulnerable parameter. Confirm the runtime language before relying on a single payload.

Pattern 1: Python rule-engine expression injection

eval(user_formula)  # user_formula = "__import__('os').system('whoami')"

Pattern 1b: exec on workflow snippet from JSON

exec(config["on_complete_hook"], globals())

Pattern 2: JavaScript / Node eval

process.mainModule.require('child_process').execSync('id')
global.process.mainModule.require('fs').readFileSync('/etc/passwd')
Function('return this')().constructor.constructor('return process')().mainModule.require('child_process').exec('id')

Pattern 3: Java ScriptEngine / Groovy

java.lang.Runtime.getRuntime().exec("id")
new java.util.Scanner(new java.io.File("/etc/passwd")).useDelimiter("\\A").next()

Pattern 4: Spring SpEL

T(java.lang.Runtime).getRuntime().exec('id')
#{T(java.lang.Runtime).getRuntime().exec('id')}
${T(java.lang.Runtime).getRuntime().exec('id')}

Pattern 5: OGNL (Struts-style)

(#rt=@java.lang.Runtime@getRuntime()).(#rt.exec('id'))
(@java.lang.Runtime@getRuntime().exec('id'))

Pattern 6: Deserialization and unsafe loaders (code execution paths)

pickle.loads(user_bytes)
yaml.load(user_yaml)  # PyYAML unsafe

Language-Specific Sinks and Dangerous APIs

Search for evaluation primitives that compile or execute user-supplied strings. Any path that concatenates request data into expression text is a review priority.

Python

result = eval(user_expr)
exec(user_code)
compile(user_snippet, "<string>", "exec")
pickle.loads(request.data)
yaml.load(body)  # without Loader=SafeLoader

Java

ScriptEngine engine = new ScriptEngineManager().getEngineByName("javascript");
engine.eval(userRule);
ExpressionParser parser = new SpelExpressionParser();
parser.parseExpression(userInput).getValue();
Ognl.getValue(userExpr, context);

C

CSharpScript.EvaluateAsync(userCode);
Microsoft.JScript.Eval.JScriptEvaluate(userExpr, engine);
Assembly.Load(userAssemblyBytes);

JavaScript

eval(req.body.expr);
new Function(userCode)();
vm.runInNewContext(userSnippet);
require('vm').runInThisContext(userInput);

Go

// Less common — review plugin/script hooks and text/template misuse:
plugin.Open(userPath)
text/template.Must(template.New("t").Parse(userTemplate)).Execute(w, data)

Shell (embedded interpreters)

python3 -c "$user_expr"
node -e "$user_js"
ruby -e "$user_ruby"

Sample Vulnerable Code in Python

from flask import Flask, request

app = Flask(__name__)

@app.route("/billing/discount")
def apply_discount():
    # Attacker-controlled pricing rule from JSON body
    rule = request.get_json().get("formula", "0")
    subtotal = request.get_json().get("subtotal", 0)
    # Sink: arbitrary Python expression evaluated with full interpreter power
    discount = eval(rule, {"subtotal": subtotal})
    return str(discount)

Step-by-Step Review Walkthrough

  1. Search for dynamic evaluation. Find eval, exec, ScriptEngine.eval, ExpressionParser, and unsafe deserialization loaders.
  2. Trace the Python (or equivalent) input path. In the sample, rule comes directly from the JSON body into eval. Ask whether any grammar restriction exists; there is none.
  3. Inspect concatenation before eval. Patterns like "Math.pow(" + x + ",2)" assemble executable text from user fragments.
  4. Review admin-only features. Formula editors and webhook transformers may be reachable through privilege escalation.
  5. Check framework expression languages. SpEL, OGNL, EL, and rule engine APIs parse user text as code.
  6. Follow sandbox claims. Verify restrictions on imports, reflection, and network access. Do not assume sandboxes are sufficient.
  7. Ask whether evaluation is required. Prefer declarative configuration, safe DSLs, or precompiled static rules.

Risk Impact Analysis

Arbitrary code execution. Full language evaluators run attacker code inside the application process with its credentials and network access.

Credential and data theft. Injected code can read environment variables, config files, database connections, and in-memory secrets.

Authorization bypass. Rules embedded in scripts may gate access; attacker-controlled expressions can force true conditions.

Persistence and lateral movement. Evaluated code may write files, spawn subprocesses, or call internal services reachable from the app tier.

Vulnerable Examples in Other Languages

Java

public boolean evaluateWorkflowRule(String userRule, Map<String, Object> ctx)
        throws ScriptException {
    ScriptEngine engine = new ScriptEngineManager().getEngineByName("groovy");
    String expression = "status == 'approved' && " + userRule;
    return (Boolean) engine.eval(expression, new SimpleBindings(ctx));
}

C

public decimal EvaluateShippingFormula(string userFormula, decimal weight)
{
    var engine = new Microsoft.ClearScript.V8.V8ScriptEngine();
    engine.AddHostObject("weight", weight);
    return Convert.ToDecimal(engine.Evaluate(userFormula));
}

JavaScript

const express = require("express");
const app = express();

app.post("/rules/test", (req, res) => {
  const formula = req.body.formula || "0";
  const ctx = { orderTotal: req.body.orderTotal };
  res.send(String(eval(`with(ctx){${formula}}`))); // attacker-controlled expression
});

Go

func evaluatePricingRule(w http.ResponseWriter, r *http.Request) {
    rule := r.FormValue("rule")
    vm := goja.New()
    vm.Set("subtotal", parseFloat(r.FormValue("subtotal")))
    val, err := vm.RunString("(" + rule + ")")
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    fmt.Fprint(w, val)
}

Fix: Safer Patterns and Libraries to Use

Python

Avoid eval and exec on untrusted strings. Use restricted evaluators or fixed server-side logic.

from simpleeval import simple_eval

@app.route("/billing/discount")
def apply_discount():
    body = request.get_json()
    rule = body.get("formula", "0")
    subtotal = body.get("subtotal", 0)
    try:
        discount = simple_eval(rule, names={"subtotal": subtotal})
    except Exception:
        return "Invalid formula", 400
    return str(discount)
# ast.literal_eval — literals only, not expressions:
import ast
data = ast.literal_eval('{"key": "value"}')  # safe for literal structures

Important: simpleeval limits semantics but is not a full sandbox for hostile admin input. Prefer fixed formulas implemented in Python for untrusted users.

Java

Replace ScriptEngine.eval on user input with arithmetic-only libraries or fixed Java code.

import net.objecthunter.exp4j.Expression;
import net.objecthunter.exp4j.ExpressionBuilder;

public double evaluateShipping(double weightKg, double distanceKm) {
    Expression expr = new ExpressionBuilder("base + weight * rate + distance * perKm")
        .variables("base", "weight", "rate", "distance", "perKm")
        .build();
    return expr.setVariable("weight", weightKg)
        .setVariable("distance", distanceKm)
        .setVariable("base", 5.0)
        .setVariable("rate", 0.8)
        .setVariable("perKm", 0.05)
        .evaluate();
}

Important: Bind variables through a sandboxed API. Never concatenate user strings into expression text.

C

Use expression parsers limited to math and boolean logic instead of full script engines.

using NCalc;

public object EvaluateFormula(string expression, Dictionary<string, object> parameters)
{
    var expr = new Expression(expression);
    foreach (var kv in parameters)
        expr.Parameters[kv.Key] = kv.Value;
    return expr.Evaluate();
}

Important: Avoid ClearScript or Roslyn on user text unless heavily sandboxed, authorized, and audited.

Go

Use a typed expression language with limited builtins instead of full JavaScript runtimes.

import "github.com/expr-lang/expr"

func evaluateFormula(input string, env map[string]interface{}) (interface{}, error) {
    program, err := expr.Compile(input, expr.Env(env))
    if err != nil {
        return nil, err
    }
    return expr.Run(program, env)
}

Important: Avoid RunString on user input in goja, Otto, or yaegi unless in a hardened sandbox with no host bindings.

Verify During Review

  • No eval, exec, ScriptEngine.eval, or equivalent processes HTTP-derived strings.
  • User "formulas" use a vetted arithmetic or rules DSL with a fixed grammar, not a general-purpose language.
  • Expression languages have restricted contexts: no arbitrary type loading, reflection, or file I/O.
  • Admin rule editors require strong authentication, authorization, and audit trails.
  • Template engines do not compile user-supplied template source at runtime.
  • Alternatives were considered: static code, configuration tables, or server-side business logic replace dynamic evaluation.

Reference