A03 Injection
A03:2021 — Injection — covers SQL, NoSQL, OS command, LDAP, XPath, XXE, ORM injection. The fix is universal: never build a query/command string by concatenating untrusted input.
Per-language parameterisation + ORM safety
EXAMPLE
// 1) SQL — parameterised everywhere
// Node (pg)
await db.query('SELECT * FROM users WHERE email = $1', [email]);
// Python (psycopg)
cur.execute('SELECT * FROM users WHERE email = %s', (email,))
// PHP (PDO)
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
// Java (PreparedStatement)
try (var ps = conn.prepareStatement("SELECT * FROM users WHERE email = ?")) {
ps.setString(1, email);
var rs = ps.executeQuery();
}
// 2) NoSQL injection — different shape, same idea
// MongoDB — operator injection via JSON body
// BAD: db.users.find(req.body.query) // attacker sends {"$ne":null} to bypass auth
// GOOD:
await db.users.findOne({ email: String(req.body.email) });
// Mongoose sanitisation:
// npm i mongo-sanitize
import sanitize from 'mongo-sanitize';
await db.users.findOne(sanitize(req.body.filter));
// 3) OS command injection — exec / spawn / system
// BAD
import { exec } from 'node:child_process';
exec(`convert ${userInput} out.png`); // shell-interpreted; attacker injects ; rm -rf /
// GOOD — use execFile (no shell)
import { execFile } from 'node:child_process';
execFile('convert', [userInput, 'out.png'], { timeout: 10_000 });
// Python — same lesson
# import subprocess
# subprocess.run('convert ' + user, shell=True) # BAD
# subprocess.run(['convert', user, 'out.png'], check=True, timeout=10) # GOOD
// Java — ProcessBuilder with arg array, never a single string
// PHP — use escapeshellarg(), or proc_open with argument array
// 4) LDAP injection — when authenticating against AD/LDAP
# BAD
filter = f'(uid=${username})'
# Attacker: username='*)(uid=*' → injects extra filter
# GOOD — escape per RFC 4515
import ldap.filter
filter = ldap.filter.filter_format('(uid=%s)', [username])
# Or use a library that escapes for you (ldap3, ldapjs)
// 5) XPath injection — same shape, different language
// BAD
String q = "//user[username='" + user + "' and password='" + pw + "']";
// Attacker: user='admin' or '1'='1
// GOOD — use parameterised XPath (e.g. javax.xml.xpath)
xpath.compile("//user[username=$user and password=$pw]")
.evaluate(doc, /* variable resolver */);
// 6) XXE (XML External Entity) — XML parsers loading external resources
// BAD — default parsers may resolve external entities
// <?xml version="1.0"?>
// <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
// <foo>&xxe;</foo>
# Defence: disable DOCTYPE / external entities
# Python (defusedxml)
import defusedxml.ElementTree as ET
tree = ET.parse(xml_file)
# Java (DocumentBuilderFactory)
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
# Node — use a parser that disables this by default (sax, xml2js with strict mode)
// 7) Template injection (SSTI) — when user input goes into a server template
// BAD
res.render('email', { body: req.body.html }); // depending on engine, can execute
// In Jinja2 / Twig / Liquid — attacker payloads like {{ 7*7 }} or {{ config.SECRET_KEY }}
# Defence: NEVER render user input as a template. Pass it as data ONLY.
res.render('email', { body: escapeHtml(req.body.text) });
// 8) Header injection — newlines in user-controlled response headers
// BAD
res.setHeader('X-Custom', userValue); // if userValue has \r\n, attacker injects headers
# Defence — strip CR/LF or use library that does (most do by default)
res.setHeader('X-Custom', userValue.replace(/[\r\n]/g, ''));
// 9) Email header injection — when building emails from forms
// BAD
mailer.send({ to: req.body.to, subject: req.body.subject });
# if `to` contains \nBcc: attacker@evil.com, the attacker becomes Bcc.
# Defence — use a real email library that validates addresses + strips control chars
# nodemailer, sendgrid, ses — all do this by default. Don't roll your own.
// 10) ORM injection — escape hatches that bypass parameterisation
// Prisma
await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`; // SAFE — tagged template
await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`); // UNSAFE
// Drizzle
await db.execute(sql`SELECT * FROM users WHERE email = ${email}`); // SAFE
// SQLAlchemy
s.execute(text('SELECT * FROM users WHERE email = :e'), {'e': email}) // SAFE
s.execute(text(f"SELECT * FROM users WHERE email = '{email}'")) // INJECTION
// Eloquent / ActiveRecord / EF Core — typed methods safe; raw / FromSqlRaw with interpolation unsafe.
// === Universal defences ===
// A) Parameterise — never concatenate untrusted strings into a query or command
// B) Allowlist identifiers (table / column / sort) — pass through a known dict
// C) Validate types + lengths at the boundary — Zod, Pydantic, DTO classes
// D) Least privilege — DB user can't DROP, can't COPY FROM PROGRAM
// E) Output encode for the destination context (HTML, URL, JSON, shell)
// F) Audit + alert — log every query / exec with structured fields, alert on anomalies
// G) WAF + bot protection at the edge — frustrates scanners; saves logs
// H) DAST in CI — OWASP ZAP, Nuclei templates for common injection patterns
// I) Code review checklist:
// • Any string concat / interpolation in a query, command, header, template?
// • If yes, can it be parameterised? If no, is the input from an allowlist?
// • Are unsafe escape hatches (queryRawUnsafe, FromSqlRaw, eval, exec with shell) banned by lint?
// === Tools ===
// semgrep — static rules for injection patterns (free, fast)
// CodeQL — deeper dataflow analysis (free for OSS)
// Snyk / Sonar — broader SAST with injection rules
// OWASP Dependency-Check / npm audit / pip-audit — catches vulnerable libs that re-introduce injection
Why it matters
Injection bugs differ by language but share the pattern: untrusted input meets a string-built command. Parameterise everything; ban the unsafe escape hatches via lint; keep allowlists for identifiers.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// A03 Injection — SQL / NoSQL / OS / LDAP / template / XSS / etc. // Fix: parameterised queries, output encoding, schema-validated input. // See the SQLi and XSS tracks for deep dives.Try it Yourself »
Exercise
OWASP Top 10 (2021) category #3 short name.
Nine letters.
Discussion
Loading…