What Is SQL Injection and How to Prevent It
Photo by Unsplash on Unsplash
Table of Contents
The Short Version
This vulnerability has been on the OWASP top ten since the list existed, which tells you something depressing about how the industry ships code.
It survives because the broken version is the version that reads most naturally. String concatenation is the obvious way to build a query, and it works perfectly until someone types a quote character.
I've reviewed enough codebases to have a rule of thumb: the vulnerable query is almost never in the data layer, it's in the one-off script somebody wrote at 6pm.
So it's worth understanding the mechanism rather than memorising a rule. Developers who know why concatenation breaks don't write it, and developers who've only memorised "use prepared statements" find creative ways around their own rule.
What an Injection Actually Is
So this:
```js db.query("SELECT * FROM users WHERE email = '" + email + "'") ```
becomes a completely different query if `email` contains a quote. Input of `' OR '1'='1` produces `SELECT * FROM users WHERE email = '' OR '1'='1'`, which matches every row in the table.
That's the whole trick. The attacker isn't exploiting a database bug — the database did exactly what it was told. The bug is that your code let input cross the line from data into syntax.
Every variant of this attack is the same idea with different punctuation. Comment sequences truncate the rest of the query. Semicolons stack a second statement. Union clauses graft on a query against a different table entirely.
A Concrete Example
```js const q = `SELECT id FROM users WHERE email='${email}' AND password='${hash}'` ```
Submit `admin@example.com'--` as the email. The query becomes `SELECT id FROM users WHERE email='admin@example.com'--' AND password='...'`, and since `--` starts a SQL comment, the password check is gone. You're logged in as admin without a password.
Now a nastier one. A search field that builds `SELECT name FROM products WHERE name LIKE '%${term}%'` accepts `' UNION SELECT password FROM users--`, and your product search starts returning password hashes in the results column.
That second example is why "this endpoint is read-only" isn't a defence. Read access to one table plus injection equals read access to every table the connection can reach.
And it doesn't stop at reading. If the driver allows stacked statements, `'; DROP TABLE users--` is a legal thing to type into a search box.
Parameterized Queries
```js db.query('SELECT id FROM users WHERE email = ? AND status = ?', [email, status]) ```
The database parses the query first, with placeholders where values go, then binds the values into those slots. A quote inside a value is now just a character in a string — it can't change the parse, because parsing already happened.
That ordering is the entire security property. Not escaping, not filtering. The value never gets a chance to be syntax.
Every mainstream driver supports this, with either `?` or `$1` style placeholders depending on the database. Named parameters are common too, and they're worth using once a query has more than three values, because positional arrays are easy to reorder by accident.
ORMs and query builders do this for you by default, which is a strong argument for using one. Just know where the escape hatch is — most expose a raw query method, and that method is where injection reappears in otherwise-safe codebases.
What Does Not Work
Numeric fields are the trap people fall into after learning about quotes. `WHERE id = ${id}` needs no quotes at all, so an escaping function does nothing and `1 OR 1=1` sails straight through.
Blocklists — rejecting input containing `SELECT`, `DROP` or `--` — break legitimate input and miss encoded variants. Someone named O'Brien can't sign up, and the attacker uses hex encoding.
Client-side validation is not a control. It's a user experience feature. Anyone can send a request directly, and the whole category of "we validate on the frontend" is why so many otherwise-modern apps are trivially exploitable.
Stored procedures help only if they use parameters internally. A stored procedure that concatenates strings is exactly as vulnerable, just harder to audit because the flaw is hidden in the database.
Defence in Depth
Least privilege comes first. The application's database user should not own the schema. If a read endpoint's connection can't drop tables, an injection there can't drop tables — and separate read and write users cost almost nothing to set up.
Validate input for shape, not for danger. An email field should contain something email-shaped and an ID should be an integer, and rejecting anything else narrows the attack surface without pretending to be a security boundary.
Error messages should not leak. A stack trace containing the failed query hands an attacker the exact table names, and blind injection becomes a lot less blind. Log the detail, return something generic. I've found more than one live vulnerability just by reading an error page that helpfully printed the failing query.
OWASP's SQL injection prevention guidance is the reference worth reading in full — it's short and it covers the edge cases like dynamic table names, which parameters genuinely cannot handle.
The Cases Parameters Can't Cover
A sortable table is the usual culprit: `ORDER BY ${column} ${direction}`. Both parts come from a query string, neither can be a placeholder.
The answer is an allowlist, mapped explicitly:
```js const cols = { name: 'name', date: 'created_at' } const col = cols[req.query.sort] ?? 'created_at' const dir = req.query.dir === 'desc' ? 'DESC' : 'ASC' ```
Now the SQL only ever contains strings you wrote. User input picks a key, it never becomes the value.
Same approach for dynamic `LIMIT` values — coerce to an integer and clamp it. `parseInt` plus a range check is a complete defence there, and it's the one place where type coercion genuinely counts as security. If you're hashing credentials in the same codebase, my notes on hashing with SHA-256 and MD5 cover the other half of the login-security story.
Auditing an Existing Codebase
Most real vulnerabilities I've found this way weren't in the main data layer — they were in a migration script, an admin report or a one-off endpoint written under deadline. The careful code is careful. The forgotten code is where the hole is.
Check the ORM escape hatches specifically. `raw`, `query`, `literal`, `unsafe` — every library has one, and each call site deserves a look.
Static analysis catches the obvious cases and misses anything indirect, so treat it as a first pass rather than a clearance. If you're formatting queries while you audit them, our note on reading SQL formatting tools covers keeping long queries readable, which makes the concatenation points much easier to spot.
Frequently Asked Questions
What is SQL injection in simple terms?
It's when user input gets treated as part of a SQL command instead of as data. Because the database parses the whole string it receives, a quote or comment character typed into a form can change what the query does — reading other tables, skipping a password check or deleting data. If you're auditing a codebase for it, our [SQL formatter guide](/blog/best-free-online-sql-formatter-2026) makes long concatenated queries far easier to read.
Do parameterized queries stop all SQL injection?
They stop injection through values, which is the overwhelming majority. They can't parameterize identifiers like table names, column names or sort direction, so dynamic ORDER BY clauses need an explicit allowlist instead.
Is escaping quotes enough to prevent SQL injection?
No. Escaping fails on numeric contexts where there are no quotes, breaks on some character sets, and depends on every single call site remembering to do it. Parameterized queries remove the possibility structurally rather than patching each instance.
Are ORMs safe from SQL injection?
Their normal query methods are, because they parameterize by default. The risk is the raw query escape hatch that every ORM provides — those calls take a plain string and reintroduce the problem, so they're worth auditing individually.
Does using a NoSQL database avoid injection?
It avoids SQL injection specifically, not injection as a category. Query-object injection is a real problem in document databases when user input is dropped straight into a filter object. The underlying rule holds everywhere: never let input become part of the query structure.
Try ToolsFuel
23+ free online tools for developers, designers, and everyone. No signup required.
Browse All Tools