Chapter 2 · Practice 1 of 5
Use parameterized queries for every database call
Why it matters
SQL injection is still one of the most reported weaknesses: OWASP's A05:2025 page counts more than 14,000 CVEs for it. The fix has not changed in twenty years. OWASP's SQL Injection Prevention Cheat Sheet ranks the defenses:
- Prepared statements with parameters, so the database treats input as data.
- Stored procedures, but only if they do not build SQL dynamically inside.
- Allowlist validation, for the parts that cannot be parameters.
- Escaping, which OWASP strongly discourages as fragile and database specific.
ORMs parameterize by default, but most have a raw query method. Those are where injection hides: sequelize.query, Prisma's $queryRawUnsafe, Django's .raw() and extra(), SQLAlchemy's text() with f-strings. Search for them in code review.
Pair this with least privilege on the database account. OWASP recommends one database user per application, no admin rights, and rarely any create or drop permissions, so a missed injection reads less and destroys nothing.
Avoid
- Template strings or f-strings that put input into SQL text
- Raw query helpers with "Unsafe" or "raw" in the name, without review
- An application database user with admin rights
Example
// Node.js with node-postgres: values travel separately from the SQL text
const { rows } = await pool.query(
'SELECT id, title FROM posts WHERE author_id = $1 AND status = $2',
[userId, status],
);
// Python with psycopg is the same idea with %s placeholders:
// cur.execute("SELECT ... WHERE author_id = %s", (user_id,))