What is SQL injection, and how to protect against it
Kort antwoord
SQL injection happens when untrusted input gets pasted directly into a database query as text, letting an attacker change what the query actually does. The standard defence is a parameterised query (also called a prepared statement), which keeps user input as data rather than letting it become part of the query's structure. It's available in essentially every modern database library, and it's the real fix, not just one layer among several.
How SQL injection actually happens
A SQL injection vulnerability starts with untrusted input, anything a user can control such as a form field, a URL parameter, or a cookie value, being concatenated directly into a database query as plain text. The application builds the query by joining strings together, sends the result to the database, and the database has no way to tell the difference between "the query the developer intended" and "the query as it now reads after the input was inserted". If that input contains SQL syntax of its own, it can change the meaning of the query entirely. An attacker who finds this can potentially read data they shouldn't see, modify records, or delete them, all through a field that was only ever meant to hold something like a username or a search term.
A simple illustrative example
Imagine a login check that builds its query like this, in plain terms: it takes the username typed into the form and inserts it directly into a string that reads something like "find the user where the username equals" whatever was typed. If a legitimate user types alice, the query ends up asking for the user named alice, which is exactly what was intended. But because the username was inserted as raw text, an attacker can type something that isn't a username at all, a fragment of SQL syntax designed to alter the query's logic, for example something that makes the "where" condition always evaluate as true regardless of what username was expected. The database doesn't see "an unusual username", it sees a modified query, and executes it as written.
Now compare that with a login check that uses a parameterised placeholder for the username instead. The query's structure, "find the user where the username equals a placeholder", is fixed and sent to the database first. The typed username is then supplied separately, as a value to slot into that placeholder, never as text that gets merged into the query itself. Whatever the attacker types is treated purely as the value being searched for, even if it contains characters that would have been dangerous in the concatenated version. The query's logic simply cannot be changed by what arrives in that field, because the database keeps the query's structure and the query's data in two separate channels from the start.
The real fix: parameterised queries
Parameterised queries, also known as prepared statements, are the standard defence, not an optional extra. They work by sending the query's structure to the database separately from the values that fill it in, so user input is always treated as data and never as part of the query's syntax, no matter what characters it contains. This isn't a niche feature: essentially every modern database library and ORM, across essentially every programming language, supports parameterised queries as a normal, idiomatic way to write a query. If a codebase is building queries by concatenating strings with user input anywhere, that's the specific pattern worth finding and replacing.
Additional layers, not substitutes
Parameterised queries are the fix for the injection mechanism itself, but two other practices are worth layering on top, as defence in depth rather than as alternatives:
- Input validation. Checking that input looks like what it's supposed to be, an email field looks like an email, a numeric ID is actually numeric, catches malformed input early. It's a useful additional check, but it's not a substitute for parameterised queries, since validation logic can be incomplete or bypassed in ways a database driver's own parameter handling won't be.
- Least-privilege database accounts. The database account an application connects with should only have the permissions that application actually needs. If a web application only ever needs to read and write specific tables, its database credentials shouldn't also carry permission to drop tables or read unrelated ones. This limits the damage if an injection flaw, or any other compromise, does happen, without preventing the flaw itself.
Both are worth doing. Neither replaces fixing the actual query-building pattern that lets injection happen in the first place.