iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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.

Test yourself

Q1. A03 (2021) covers…
Q2. The dominant defence is…
Q3. XSS belongs to which category in 2021?

Discussion

Loading…