The Attack
SQL injection happens when user input is inserted directly into a SQL string, allowing an attacker to modify the query's logic. It's the single most dangerous vulnerability in web applications — and the most preventable.
Imagine you build a login form and construct the SQL query by concatenating the username:
If a user enters a normal username like alice, you get a valid query:
But what if someone enters this as their "username"?
User input: ' OR '1'='1' -- Resulting query: SELECT * FROM users WHERE username = '' OR '1'='1' --' What the database sees: username = '' → false OR '1'='1' → always true! -- → comment (ignores the rest) Result: Returns ALL users. Attacker is "logged in" as the first user.
Destructive Injection
It gets worse. An attacker could enter input designed to destroy your data entirely:
User input: '; DROP TABLE stories; -- Resulting query: SELECT * FROM users WHERE username = ''; DROP TABLE stories; --' What the database executes: 1. SELECT * FROM users WHERE username = '' (returns nothing) 2. DROP TABLE stories (deletes your entire table!) 3. --' (rest is a comment) Your stories table is gone.
The Fix: Parameterized Queries
Parameterized queries (also called prepared statements) separate the SQL structure from the data values. The database treats parameters as data, never as SQL code — so malicious input can't change the query's logic.
WRONG vs RIGHT — Node.js
Here is a vulnerable Node.js query next to its safe equivalent:
WRONG — String Concatenation:
RIGHT — Parameterized Query:
WRONG vs RIGHT — PHP
The same pattern applies to PHP — string concatenation is vulnerable, prepared statements are safe:
WRONG — String Concatenation:
RIGHT — Prepared Statement:
How Parameterized Queries Work Under the Hood
The key difference is that parameterized queries use a two-step process at the database protocol level:
String Concatenation (VULNERABLE):
Your code: "SELECT * FROM users WHERE name = '" + input + "'"
Database sees: One big string to parse as SQL
Problem: Input becomes part of the SQL structure
Parameterized Query (SAFE):
Step 1 - PREPARE: Database parses "SELECT * FROM users WHERE name = $1"
SQL structure is locked in — one condition, one value
Step 2 - EXECUTE: Database receives $1 = "'; DROP TABLE users; --"
This is treated as a data value, period.
Cannot change the query structure.
With parameterized queries, the SQL structure is parsed and compiled before the values are provided. By the time the user data arrives, the database has already decided what the query does — the values can only fill in data slots, never alter the query logic.
Other Injection Prevention Practices
While parameterized queries are the primary defense against SQL injection, a defense-in-depth approach includes additional layers:
- Input validation: Verify that IDs are integers, emails match expected formats, and strings don't exceed expected lengths before they reach the database layer.
- Least privilege: The database user your application connects with should only have the permissions it needs (SELECT, INSERT, UPDATE, DELETE on specific tables) — never grant superuser access.
- ORMs and query builders: Tools like Sequelize, Prisma, Eloquent, and Doctrine generate parameterized queries automatically, reducing the chance of manual mistakes.
- Web Application Firewalls (WAFs): Can detect and block common injection patterns at the network level, providing an additional safety net.