SQL Injection: How It Works and How to Prevent It
SQL injection is one of the oldest web vulnerabilities, and it still appears in modern applications, often in places developers assume are safe: sort parameters, search filters, reporting features and raw queries added "just this once."
How SQL injection works
SQL injection happens when user input is concatenated into a SQL query, so the database treats part of the input as code.
// Vulnerable: user input becomes part of the SQL
const q = "SELECT * FROM users WHERE email = '" + req.body.email + "'";
If an attacker submits ' OR '1'='1, the query becomes:
SELECT * FROM users WHERE email = '' OR '1'='1'
The condition is always true, and the query returns every user.
What attackers can do with it
- Read data: dump users, password hashes, API keys and customer records.
- Bypass authentication on poorly written login queries.
- Modify or delete data.
- Escalate further: on some databases and configurations, read files or execute commands on the server.
Common variants
- In-band (classic): results or errors appear directly in the response.
- Union-based: attackers append
UNION SELECTto pull data from other tables. - Error-based: database error messages leak data.
- Blind boolean-based: the page changes subtly depending on whether a condition is true.
- Blind time-based: attackers infer data by making the database delay, for example with
SLEEP(). - Second-order: malicious input is stored safely, then later used unsafely in another query.
Where it hides in modern apps
ORMs prevent most SQL injection, but not all. Watch for:
- Raw query helpers used for complex reports.
- ORDER BY and column names, which cannot be parameterised and are often built from query strings.
- Search and filter builders that accept operators from the client.
- JSON and array operators passed into raw SQL fragments.
- Stored procedures that build dynamic SQL internally.
How to prevent SQL injection
1. Use parameterised queries everywhere.
// Safe: the driver sends the value separately from the query
const { rows } = await db.query("SELECT * FROM users WHERE email = $1", [req.body.email]);
2. Allowlist identifiers. For sort fields and column names, map user input to a fixed set:
const SORTS = { newest: "created_at DESC", name: "name ASC" };
const order = SORTS[req.query.sort] ?? SORTS.newest;
3. Use least-privilege database accounts. The application user should not be able to drop tables or read system files.
4. Avoid leaking database errors to users. Log them server-side instead.
5. Review every raw query in code review, and flag string concatenation in SQL with a linter rule.
Testing for SQL injection
Testing means trying injection payloads in every parameter, including headers, cookies, JSON bodies and hidden fields, and observing differences in responses, errors and timing. Blind injection requires careful comparison, which is where automated testing shines.
SecurityInfinity agents test every discovered parameter for SQL injection, confirm exploitability without destructive payloads, and give the exact fix for the affected code path. Start free.
Frequently asked questions
Do ORMs completely prevent SQL injection?
They prevent most of it when used normally, but raw queries, dynamic column names and some advanced features can still be vulnerable.
Is input validation enough to stop SQL injection?
No. Validation helps, but parameterised queries are the primary defence. Validation alone is easy to bypass.
Does a web application firewall stop SQL injection?
A WAF can block common payloads, but attackers regularly find bypasses. Fix the code; treat a WAF as an extra layer.