EncryptCodecencryptcodec
Blog/Security
SecurityApril 8, 2026 · 9 min read

SQL Injection Prevention — A Practical Guide with Code Examples

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.

SQL Injection Simulator
User-supplied input
alice@example.com
→ concatenated into SQL string
SQL sent to database
SELECT * FROM users WHERE email = 'alice@example.com'
SQL structure
safe value
injected SQL
A legitimate email address. The query returns exactly one user row.
Result rows:
1
1 row returned

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:

// Node.js with 'pg' (PostgreSQL)
const { rows } = await client.query(
'SELECT * FROM users WHERE email = $1 AND status = $2',
[req.body.email, 'active']
);

// Node.js with 'mysql2'
const [rows] = await connection.execute(
'SELECT * FROM users WHERE email = ? AND status = ?',
[req.body.email, 'active']
);

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:

const ALLOWED_SORT_COLUMNS = ['name', 'created_at', 'email'];

const sortBy = ALLOWED_SORT_COLUMNS.includes(req.query.sort)
? req.query.sort
: 'created_at';

// Safe to interpolate because it came from your allowlist, not user input
const { rows } = await client.query(
`SELECT * FROM users ORDER BY ${sortBy} ASC`
);

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.

// Sequelize — UNSAFE raw query
const users = await sequelize.query(
`SELECT * FROM users WHERE name = '${req.body.name}'`
);

// Sequelize — SAFE raw query with replacements
const users = await sequelize.query(
'SELECT * FROM users WHERE name = :name',
{ replacements: { name: req.body.name }, type: QueryTypes.SELECT }
);

// Sequelize — SAFE ORM method (parameterized internally)
const users = await User.findAll({
where: { name: req.body.name }
});

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 SELECT or 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.

Share this post

Try the SQL Injection Simulation

Free, browser-based — no signup required.

Frequently Asked Questions

Related posts