SQL injection: when data becomes instructions
String concatenation turns user input into executable database commands. Why escaping strings fails and parameterized queries always win.
SQL injection (SQLi) is one of the oldest vulnerabilities in web development, yet it remains near the top of OWASP Top 10 lists year after year.
The root cause is always the same: mixing code and untrusted data in the same string.
How concatenation breaks syntax
Consider a login query built with string interpolation:
// Vulnerable code
const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${passwordHash}'`;
If a user enters an email like admin@example.com, the query behaves normally. But if the input contains quotes and SQL keywords:
admin@example.com' OR '1'='1' --
The string interpolates directly into the query template:
SELECT * FROM users WHERE email = 'admin@example.com' OR '1'='1' --' AND password = '...';
Because '1'='1' is always true, and -- comments out the rest of the query, the database returns the first user row (usually the admin) and bypasses password authentication completely.
Why manual string escaping is a trap
Many developers try to fix this by replacing single quotes or filtering out keywords like UNION, SELECT, or DROP:
// Fragile!
const sanitized = input.replace(/'/g, "\\'");
Blacklisting fails because:
- Attackers use alternative encodings (
0x27, unicode variants, or null bytes). - Numeric inputs don’t need quotes at all:
SELECT * FROM products WHERE id = ${id}allowsid = 1; DROP TABLE products;. - Second-order injections can slip through when escaped strings are read from the database and used in a secondary dynamic query.
The real fix: Prepared statements (Parameterized queries)
A prepared statement sends the SQL query structure and the user parameters in two completely separate communication steps to the database engine:
Step 1 (Parse & Plan): SELECT * FROM users WHERE email = $1 AND password = $2
Step 2 (Execute data): $1 = "admin@example.com' OR '1'='1' --", $2 = "hash"
// Secure parameterized query
const result = await db.query(
'SELECT id, role, email FROM users WHERE email = $1 AND password_hash = $2',
[email, passwordHash]
);
Because the database compiler parses the query AST (Abstract Syntax Tree) in Step 1, the inputs passed in Step 2 are treated strictly as literal string values. Even if the input contains SQL syntax, it will never be parsed as executable commands.
Summary checklist
| Practice | Safety | Why |
|---|---|---|
String concatenation (+, template literals) | ❌ Dangerous | User input alters SQL command structure |
| Custom regex string replacement | ⚠️ Fragile | Easy to bypass with encodings and edge cases |
Parameterized queries ($1, ?, :val) | ✅ Bulletproof | Compiles code and data separately |
| Modern ORMs (Prisma, Drizzle, TypeORM) | ✅ Safe by default | Use prepared statements under the hood (unless raw SQL interpolation is used) |
Test Your Understanding
Quick architectural scenario challenge based on this breakdown.
A 3rd-party payment gateway starts timing out with 504 errors during peak traffic.
Q1.How should a downstream dependency failure be handled to protect upstream service availability?
Comments –
Sign in to share your thoughts
Nothing yet. Be the first.