Web Security Cheatsheet
Injection and SQLi
Use this Web Security reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
What injection is
Injection happens when untrusted input reaches an interpreter as code instead of data. SQL, shell, LDAP, XPath, template engines, and log files are all interpreters, so they all share the shape and the class of fix. A05:2025 covers the whole family.
# Vulnerable: the input becomes part of the query text q = "SELECT * FROM users WHERE email = '" + email + "'" cur.execute(q)
The user supplies a value, but the database receives a program. No amount of inspecting that string tells you whether it is safe, because the boundary between code and data was already destroyed.
Parameterized queries, the actual fix
Send the query and the values on separate channels. The driver hands the statement to the database once and binds the values as typed parameters, so a value can never change the statement's structure.
# Python, DB-API. Note: no quotes around the placeholder. cur.execute("SELECT * FROM users WHERE email = %s", (email,)) cur.execute("SELECT * FROM users WHERE id = %s AND org = %s", (uid, org))
// Node, node-postgres await client.query('SELECT * FROM users WHERE email = $1', [email]) // Node, mysql2, prepared statement await conn.execute('SELECT * FROM users WHERE email = ?', [email])
// Java, JDBC PreparedStatement ps = conn.prepareStatement( "SELECT * FROM users WHERE email = ?"); ps.setString(1, email); ResultSet rs = ps.executeQuery();
// Go, database/sql row := db.QueryRow("SELECT id FROM users WHERE email = $1", email)
// PHP, PDO. Turn emulation OFF so these are real prepares. $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $st = $pdo->prepare('SELECT * FROM users WHERE email = ?'); $st->execute([$email]);
Quoting the placeholder ('%s', '?') defeats the whole mechanism, because you
are back to string interpolation with extra steps.
Why escaping and allowlists are not the fix
Both are frequently proposed and both fail as a primary control.
| Proposed control | Why it fails |
|---|---|
Manual escaping (addslashes, hand-rolled quote doubling) | Correctness depends on the exact dialect, charset, and column type. GBK and other multibyte encodings have historically let a quote survive escaping. |
A "dangerous keyword" denylist (DROP, UNION, --) | Denylists enumerate badness, which is unbounded. Comment syntax, casing, whitespace, and encoding variants all evade them, and legitimate input like the surname O'Brian or a bio mentioning "union" gets rejected. |
| Input validation alone | Correct and necessary, but it is a type check, not a boundary check. A perfectly valid integer still concatenates into a query. |
| ORM by default | Most ORMs parameterize, but every one has a raw-SQL escape hatch that does not. |
| Stored procedures | Only safe if the procedure itself parameterizes. A procedure that builds dynamic SQL internally is just as vulnerable. |
Validation is defense in depth and worth doing. Parameterization is the control.
What cannot be parameterized
Placeholders bind values only. Table names, column names, ORDER BY
expressions, and LIMIT in some drivers are part of the statement structure, so
they need a strict allowlist mapping.
# Wrong: no placeholder can carry an identifier cur.execute(f"SELECT * FROM users ORDER BY {sort} {direction}") # Right: map the input to a fixed set of known-safe fragments SORTS = {"name": "name", "created": "created_at", "email": "email"} DIRS = {"asc": "ASC", "desc": "DESC"} col = SORTS.get(sort, "created_at") dir_ = DIRS.get(direction, "ASC") cur.execute(f"SELECT * FROM users ORDER BY {col} {dir_} LIMIT %s", (limit,))
The allowlist is safe here because the value that reaches the SQL text is one you wrote, never one the user sent. This is the only correct use of an allowlist in this context.
Injection in other interpreters
Same bug, different interpreter. The fix is always an API that keeps data out of the parsed text.
OS commands. Pass an argument vector, never a shell string.
import subprocess # Vulnerable: a shell parses the whole string subprocess.run(f"convert {name} out.png", shell=True) # Fixed: argv, no shell involved subprocess.run(["convert", name, "out.png"], shell=False, check=True)
Server-side template injection. Never build a template from user input.
# Vulnerable: input becomes template source render_template_string("Hello " + name) # Fixed: input is a variable passed to a fixed template render_template("hello.html", name=name)
NoSQL. Coerce types, because an object where a string is expected turns into operator syntax.
// Vulnerable: req.body.email may be an object, not a string await User.findOne({ email: req.body.email }) // Fixed: force the type first await User.findOne({ email: String(req.body.email ?? '') })
Log injection. Untrusted newlines forge log entries and can reach a log parser. Log structured records with escaped fields rather than concatenated text.
log.info("login_failed", extra={"email": email, "ip": ip})
Recognizing it in review
Search for the shape, not the symptom. These greps find most real cases.
# String building around SQL keywords grep -rniE "(select|insert|update|delete).*(\+|%s?\"|f\"|\\$\{|\\.format\()" src/ # Raw escape hatches in ORMs grep -rnE "raw\(|execute\(f|literal\(|text\(|createQueryBuilder.*where\(" src/ # Shells and eval grep -rnE "shell=True|exec\(|eval\(|child_process\.exec\(" src/
Review checklist:
- Every query uses placeholders, and no placeholder is wrapped in quotes.
- Any identifier interpolated into SQL comes from a hardcoded map, not from input.
- ORM raw-SQL calls still pass bind parameters.
- subprocess/child_process calls use an argv array with no shell.
- Database accounts are least privilege, so the app user cannot DROP or read
tables it never queries.
- Errors are generic to the client. A verbose driver error is a free schema map,
so log the detail server side and return a request id.
- The app does not build templates, regexes, or file paths from user input.
Further reading: the OWASP SQL Injection Prevention Cheat Sheet and Query Parameterization Cheat Sheet.