You reviewed a pull request last week and saw this line:
db.query(`SELECT * FROM users WHERE email = '${req.body.email}'`);You flagged it, the dev said "we validate the input," and the PR got merged anyway. That's how breaches happen. Use parameterized queries everywhere user data touches a SQL statement — no exceptions. Input validation is a supplement, not a replacement.
SQL injection has been on OWASP's top-ten list for over a decade and still shows up in production code constantly. The fix is straightforward, but the failure modes are subtle enough that developers keep getting it wrong.
Why String Interpolation in SQL Is Never Safe
When you concatenate user input directly into a SQL string, you're trusting that the input contains no SQL syntax. It always eventually will — either from a malicious user or a well-meaning one who puts an apostrophe in their last name.
Classic example:
SELECT * FROM users WHERE email = 'foo@bar.com' OR '1'='1'That returns every row in the table. And that's the simple case. Advanced payloads use UNION SELECT, time-based blind injection via SLEEP(), and stacked queries to dump schemas, read files, or execute OS commands depending on the database and driver configuration.
The 2008 Heartland Payment Systems breach — 130 million card numbers — was SQL injection. The 2009 RockYou breach that leaked 32 million plaintext passwords: also SQL injection. This isn't theoretical.
Parameterized Queries — The Right Pattern
The fix is to send the SQL structure and the data as separate payloads. The database driver handles escaping at the protocol level, not the string level. Here's how it looks across the common backend stacks:
Notice the pattern: the SQL string is a compile-time constant and all user data is passed as a separate argument. The driver — not your code — handles escaping at the wire protocol level.
Where Developers Still Get Burned
Even teams that know about prepared statements introduce injection vulnerabilities in a few specific places.
Dynamic ORDER BY and column names can't be parameterized because placeholders only work for values, not identifiers. If you let users sort by column, you must use an allowlist:
Raw query escape hatches in ORMs are the other common footgun. Sequelize's sequelize.query(), Hibernate's createNativeQuery(), SQLAlchemy's text() — all of these bypass parameterization if you interpolate into them. They have their own parameterization syntax; use it.
Logging and error messages can also reveal SQL structure to an attacker. Never return raw database errors to the client. Catch the exception, log it server-side with a correlation ID, and return a generic 500 to the user.
ORM Usage — Safe by Default, Until You Override It
ORMs protect you when you stay in their query builder APIs. The danger is when developers reach for raw SQL because they need a complex query and forget to parameterize.
Stored Procedures Aren't Automatically Safe
A lot of developers assume that moving SQL into stored procedures eliminates injection risk. It doesn't — if the stored procedure builds dynamic SQL internally using string concatenation, you've just moved the vulnerability one layer deeper. Stored procedures are safe when they use parameterized statements internally, not when they exist.
Defense in Depth: What Goes Beyond Parameterization
Parameterized queries solve injection at the query level. These additional controls limit the blast radius if something slips through:
- Least-privilege database accounts — your app user should only have SELECT/INSERT/UPDATE on the tables it needs. It should never have DROP, TRUNCATE, or FILE privileges.
- Web Application Firewall (WAF) — catches common payloads in transit, but treat it as a detection layer, not a prevention layer.
- Database activity monitoring — alerts on anomalous query patterns like
UNION SELECTor large result sets from unusual sources. - Schema validation on input — reject malformed emails, enforce length limits, use strict type coercion. This doesn't prevent injection on its own, but it catches mistakes early and reduces noise in your logs.
Testing Your Own Code
Don't wait for a penetration test. Run sqlmap against your staging environment — it's free, it's fast, and it will find injectable endpoints in minutes if they exist. Point it at an endpoint that accepts user input and let it run with --level=3 --risk=2. If it finds nothing, you're likely in good shape.
Also audit your codebase with a simple grep:
grep -rn "query\|execute\|prepare" src/ | grep -E "\+|\`.*\$\{|\.format\("Any match that combines a SQL keyword with string interpolation is worth reviewing manually.
Go run sqlmap against your staging login endpoint right now — if you've never done it, the results will surprise you.
Frequently Asked Questions
Related posts
Secure Password Reset Tokens — Expiry, Storage, and What Most Implementations Get Wrong
A practical guide to building secure password reset flows: token generation, expiry windows, one-time use enforcement, and the edge cases that cause real account takeovers.
Mar 30, 2026 · 7 min readIncident Response for Developers: What to Do When You Get Hacked
A practical incident response guide for developers covering detection, containment, eradication, recovery, and communication when a security breach happens.
Mar 29, 2026 · 9 min readPhishing Prevention: A Developer's Guide to SPF, DKIM, and DMARC
Understand how email spoofing enables phishing attacks and how to implement SPF, DKIM, and DMARC to protect your domain from being impersonated.
Mar 29, 2026 · 9 min read