SQL Injection Explained: How It Works and How to Prevent It
SQL injection is still one of the most damaging web vulnerabilities. Learn how attackers exploit it, why string-built queries are dangerous, and the practical defences that stop it.
SQL injection happens when an application builds a database query by joining untrusted input into the SQL text. If an attacker can control part of that text, they can change what the query does — read data they should not see, modify records, or in some configurations take over the database server.
It is one of the oldest web vulnerabilities, and it still appears in real applications because the vulnerable pattern is easy to write by accident. The good news is that the main defence is simple and reliable.
How SQL injection works
Consider a login check that builds its query with string concatenation:
// Vulnerable: user input becomes part of the SQL text
$sql = "SELECT id FROM users WHERE email = '" . $_POST['email'] . "'";
$result = $db->query($sql);
If someone submits the email value ' OR '1'='1, the database receives:
SELECT id FROM users WHERE email = '' OR '1'='1'
The condition is now always true. The input was meant to be data, but the database interpreted it as code. Every form of SQL injection comes back to this confusion between data and query structure.
Common types of SQL injection
- In-band (classic): the results of the injected query appear directly in the page, for example through an error message or a
UNIONthat adds rows to the output. - Blind: nothing is displayed, but the attacker infers information from differences in the response — a page that loads versus one that does not (boolean-based), or a delayed response (time-based).
- Second-order: malicious input is stored safely at first, then later used unsafely in a different query.
The primary defence: parameterised queries
Parameterised queries (also called prepared statements) send the SQL structure and the values to the database separately. The database parses the query first, so a value can never change its structure — it is always treated as data.
// Safe: the value is bound as a parameter, never parsed as SQL
$stmt = $pdo->prepare('SELECT id FROM users WHERE email = ?');
$stmt->execute([$_POST['email']]);
$user = $stmt->fetch();
The same principle exists in every mainstream language and database driver: placeholders such as ?, :name or $1, with values passed separately. Object-relational mappers (ORMs) use parameters for normal operations too — but most also offer "raw query" functions, where the same rules apply.
Rule of thumb: if you are building SQL by concatenating or interpolating variables, stop and use a parameter instead.
When parameters are not enough
Parameters can only replace values. They cannot replace identifiers such as table names, column names, or keywords like ASC and DESC. If those need to vary — for example, a user-selectable sort column — map the input to a fixed list of allowed values:
$allowed = ['created_at', 'title', 'views'];
$column = in_array($_GET['sort'] ?? '', $allowed, true) ? $_GET['sort'] : 'created_at';
$direction = ($_GET['dir'] ?? '') === 'asc' ? 'ASC' : 'DESC';
$sql = "SELECT id, title FROM posts ORDER BY $column $direction";
Here, user input never reaches the query directly: it only selects one of the values you wrote yourself.
Defence in depth
Parameterised queries are the fix. These measures reduce the impact if something is missed:
| Measure | Why it helps |
|---|---|
| Least-privilege database accounts | The web application's database user should only have the permissions it needs. A read-only reporting page does not need DROP or GRANT. |
| Input validation | Checking that an ID is an integer or an email is well formed catches mistakes early. It complements parameters; it does not replace them. |
| Generic error messages | Detailed database errors shown to users help attackers map your schema. Log the details; show a generic message. |
| Code review and testing | Search code for string-built queries, and include injection checks in security testing. |
What does not work on its own
- Escaping quotes manually. It is easy to get wrong across character sets and contexts, and it does nothing for numeric inputs that are not quoted.
- Blocking "dangerous" words. Blocklists are bypassed with alternative syntax and encodings.
- Relying only on a web application firewall. A WAF can be a useful extra layer, but it does not fix the vulnerable code.
Quick checklist
- Use parameterised queries for every value that reaches SQL.
- Allow-list any identifier or keyword that must vary.
- Give the application's database user the minimum privileges it needs.
- Never show raw database errors to visitors.
- Review raw-query code paths in your ORM as carefully as hand-written SQL.
Further reading
The OWASP SQL Injection Prevention Cheat Sheet is an excellent, regularly maintained reference with examples for many languages.
How Wintecsys can help
Every website and application we build uses parameterised queries, least-privilege database accounts and safe error handling from the first line of code. If you already have a website, online store or business system and you are not sure it is safe, we can review it and fix the issues we find.
See our security and maintenance services or tell us about your project.