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 controlWhy 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 aloneCorrect and necessary, but it is a type check, not a boundary check. A perfectly valid integer still concatenates into a query.
ORM by defaultMost ORMs parameterize, but every one has a raw-SQL escape hatch that does not.
Stored proceduresOnly 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.