Sharon Ben-Moshe is the founder of sqlfmt, a browser-based SQL formatter and validator with misspelled-keyword suggestions across PostgreSQL, MySQL, SQLite, SQL Server, and BigQuery.
Learn how to completely prevent SQL injection. Understand parameterized queries, prepared statements, and safe dynamic SQL identifier quoting.
Quick answer
SQL injection is completely prevented by using parameterized queries (prepared statements) for all user-supplied data values. Parameters separate SQL code structure from user data at the database protocol level, ensuring data is never parsed as executable SQL commands. For dynamic identifiers (table or column names), sanitize using native quoting functions like quote_ident() or strict allowlists.
String escaping fails when multibyte character sets (e.g. GBK or Big5) trick character decoders, or when unquoted numeric inputs bypass quotes entirely (`WHERE id = 1 OR 1=1`). Parameterized queries bypass string parsing completely.
Can column and table names be parameterized in SQL prepared statements?
No. Parameter markers (`?`, `$1`, `@param`) only represent literal values. They cannot replace table names, column names, or SQL keywords. Dynamic identifiers must be validated against a hard-coded allowlist or safely quoted.
How do ORMs protect against SQL injection?
Modern ORMs (such as Drizzle, Prisma, EF Core, and ActiveRecord) use parameterized queries by default under the hood. However, raw SQL escape hatches (`db.execute(sql)`) remain vulnerable if developers interpolate strings directly.
What is second-order SQL injection?
Second-order SQL injection occurs when malicious input is safely stored in the database initially, but later read out and concatenated into a dynamic SQL query without parameterization.
SQL injection remains one of the most critical vulnerabilities in the OWASP Top 10. Understanding how prepared statements separate code from data—and how to handle dynamic identifiers safely—is mandatory for every software engineer.
Key takeaways
Always use parameterized queries for user values ($1, ?, @param). Never interpolate strings.
Parameters only work for values, not identifiers (table or column names).
Validate dynamic table and column names against a strict compile-time allowlist.
In PostgreSQL PL/pgSQL, use EXECUTE ... USING for parameters and quote_ident() for identifiers.
Vulnerable string concatenation versus prepared statements
When user input is directly concatenated into a query string, attackers can hijack the query structure:
Vulnerable code versus parameterized query
-- VULNERABLE: Direct string concatenation-- If input is: ' OR '1'='1' ---- The query returns all users and bypasses password authentication:SELECT * FROM usersWHERE email = 'user@example.com' AND password = '' OR '1'='1' --';-- SECURE: Parameterized Query (Node.js pg / Drizzle / PostgreSQL)-- The database treats the input strictly as a literal text value:SELECT * FROM usersWHERE email = $1 AND password_hash = $2;
Handling dynamic table and column names safely
When user interfaces allow sorting by different columns, parameter markers cannot be used for the column name. Use a strict allowlist:
Safe dynamic column sorting using an allowlist in TypeScript/Node
// Safe column allowlistconst ALLOWED_SORT_COLUMNS = new Set(['created_at', 'order_total', 'customer_name']);function buildSortedQuery(userSortColumn: string) { // Reject any value not in the strict allowlist if (!ALLOWED_SORT_COLUMNS.has(userSortColumn)) { throw new Error('Invalid sort column'); } // Safe to interpolate because userSortColumn is strictly validated return `SELECT id, order_total FROM orders ORDER BY ${userSortColumn} DESC`;}