Back to Cheat Sheets

💉 InjectionAPI 2019

OWASP API Security Top 10 2019 - API8

CRITICAL RISK

📋 What Is It?

Injection occurs when untrusted data from an API request is sent to a downstream interpreter as part of a command or query, and the interpreter cannot tell the attacker's data apart from the developer's instructions. The root cause is always the same shape: a string or object built from request input is interpreted rather than treated as inert data. The interpreter changes - SQL engine, MongoDB query planner, OS shell, LDAP directory, XPath parser, log sink - but the mechanism does not.

APIs sharpen the problem: they accept structured input, so an attacker can inject whole objects ({"$ne": null}), they expose client-driven sort/filter/fields semantics, they are machine-to-machine so hostile input is never filtered by a browser, and a single endpoint may chain to several interpreters at once.

API8 OWASP Rank 2019
Many Interpreters Per Route
RCE Worst Case

⚠️ Common Exploits

  • SQL Injection: concatenated WHERE clauses, dynamic ORDER BY
  • NoSQL Injection: a string field arriving as an operator object
  • Command Injection: request values spliced into a shell string
  • LDAP / XPath Injection: filters built by concatenation
  • Template & Expression Injection: user text rendered as a template
  • Header & Log Injection: CRLF and forged log lines
  • ORM Escape Hatches: raw-SQL helpers reached with unbound input

🔴 Attack Flow

1. Map every parameter: path, query, headers, JSON/XML body fields
↓
2. Probe with metacharacters and operator objects (', ;, {"$ne":null})
↓
3. Watch for errors, timing changes, or altered result sets
↓
4. Shape a payload for the interpreter that answered
↓
5. IMPACT: Auth bypass, whole-table exfiltration, or command execution!

❌ Vulnerable Code

# Bad: SQL built by concatenation q = f"SELECT * FROM users WHERE name = '{request.args['name']}'" db.execute(q) ← ' OR '1'='1 → every row // Bad: NoSQL - the field is typed as a string but an object arrives db.users.findOne({ email: req.body.email, password: req.body.password }); // {"password": {"$ne": null}} authenticates as the first matching user # Bad: shell string built from input subprocess.run(f"convert {filename} out.png", shell=True) ← "a.png; curl …|sh" // Bad: client-driven sort spliced into the query const rows = await db.query(`SELECT * FROM items ORDER BY ${req.query.sort}`); // Bad: user text rendered as a template res.send(ejs.render(req.body.template, ctx)); ← server-side template injection

✅ Secure Code

# Good: parameter binding - data can never become code db.execute('SELECT * FROM users WHERE name = %s', (request.args['name'],)) // Good: force the expected type before it reaches the query const { email, password } = CredentialsSchema.parse(req.body); // strings only const user = await db.users.findOne({ email }); if (!user || !(await argon2.verify(user.hash, password))) return res.sendStatus(401); # Good: no shell, arguments passed as a list subprocess.run(['convert', filename, 'out.png'], shell=False, check=True) // Good: identifiers come from an allow-list, never from input const SORTABLE = { name: 'name', created: 'created_at' }; const col = SORTABLE[req.query.sort] ?? 'created_at'; const rows = await db.query(`SELECT * FROM items ORDER BY ${col}`); # Good: least privilege at the data layer limits the blast radius # GRANT SELECT, INSERT ON app.* TO 'api_user'@'%'; -- no DROP, no FILE

✓ Prevention Checklist

  • Use parameterised queries or a safe ORM binding for every interpreter
  • Validate input against a strict schema - type, shape, length, and range
  • Reject objects where a scalar is expected (the NoSQL operator trick)
  • Allow-list identifiers: table, column, sort field, file path, command
  • Never build shell strings; pass argument arrays with shell=False
  • Escape or avoid concatenation in LDAP, XPath, and template contexts
  • Encode output for the sink it is heading into
  • Grant the database account the least privilege it needs
  • Audit ORM raw-query escape hatches in code review and with SAST
  • Log and alert on rejected input patterns rather than silently dropping them

🔍 Detection & Tools

Testing:

sqlmap NoSQLMap Burp Suite OWASP ZAP commix

Code & Runtime:

Semgrep CodeQL Pydantic / Zod WAF (defence in depth)

How to Test:

  • Fuzz every parameter with quotes, semicolons, and comment markers
  • Send an operator object where a string is expected
  • Look for timing differences (SLEEP, pg_sleep) on blind paths
  • Test sort, filter, and fields parameters specifically
  • Grep the codebase for string-built queries and shell=True

🌍 Real-World

These are documented incident classes - recurring patterns rather than single CVEs.

  • Login bypass via NoSQL operators: a JSON body sends {"$ne": null} where a password string was expected and authenticates without a credential.
  • Mass exfiltration through SQLi: one concatenated filter parameter dumps entire tables via UNION or blind extraction.
  • Command injection in media pipelines: filenames or conversion options passed to a shell allow arbitrary commands on the worker.
  • Template injection in "customisable" APIs: user-supplied templates rendered server-side lead to remote code execution.
  • ORM escape hatches: a single raw()/literal() call with unbound input reintroduces SQLi into an otherwise safe codebase.

📌 Quick Tips

  • DO NOT concatenate input into any query or command
  • DO NOT assume a JSON field is the type you declared
  • DO bind parameters and validate with a schema
  • DO allow-list identifiers and sort fields
  • DO run the database as a least-privilege user

📜 Compliance

Related Standards:

  • OWASP API Security Top 10 2019 - API8
  • 2023 note Injection was folded into API8:2023 Security Misconfiguration & general secure-coding guidance
  • CWE-89 SQL Injection
  • CWE-943 Improper Neutralization in Data Query Logic (NoSQL)
  • CWE-78 OS Command Injection
  • CWE-1336 Server-Side Template Injection