SQL Injection, Explained: From Login Bypass to Full Data Extraction
SQL injection is still one of the most damaging web vulnerabilities. Here is how it actually works, what an attacker can do with it, and the one fix that reliably stops it.
' OR '1'='1' -- -At a glance
CWE-89- Interpreter
- SQL database engine
- Required condition
- Attacker-controlled data reaches dynamically constructed SQL syntax.
- Potential impact
- Authentication bypass, unauthorized reads or writes, and possible database compromise.
- Primary defense
- Parameterized queries with typed values, plus allow-listed identifiers and least privilege.
Safe practice: use benign proof only, in a disposable local lab or on a system you are explicitly authorized to test.
On this page
SQL injection (SQLi) has remained a major web risk for decades. In the OWASP Top 10:2025 it sits inside A05 Injection, a category found across every application set OWASP measured. SQLi happens whenever untrusted input is mixed directly into SQL syntax, letting an attacker change the meaning of a query. The result ranges from bypassing a filter to reading or changing everything the application's database account can reach.
This post walks through how SQLi works, what an attacker can actually do with it, and how to shut it down for good.
The mental model: data crosses into query structure
A database should receive two separate things: a fixed SQL statement and a collection of values to bind into it. Injection appears when the application sends one assembled string instead. The database cannot tell which characters came from the developer and which came from the request; it parses all of them as one program.
| Stage | Safe design | Injectable design |
|---|---|---|
| Application owns | SQL structure | Only the beginning of a SQL string |
| Request controls | Bound values | Characters inside the final SQL string |
| Database sees | Code and data separately | One combined statement |
| Result | Input cannot create operators | Quotes, comments and operators can change logic |
This distinction matters more than any particular payload. If request data can become a keyword, quote, comment, identifier or operator in the statement the database parses, the boundary has already failed.
How it happens
The root cause is usually string concatenation. Consider a legacy login handler that performs both account lookup and password comparison inside a query it builds from request values:
$username = $_POST['username']; $password = $_POST['password']; $query = "SELECT id, role FROM users WHERE username = '" . $username . "' AND password_hash = SHA2('" . $password . "', 256)"; $result = $db->query($query);
The application expects username to be something like alice. Nothing forces it to be. This handler also illustrates a second design problem: modern authentication should fetch a stored password hash and verify it with a password-hashing API, not recreate a fast general-purpose hash in SQL.
A concrete attack: authentication bypass
Suppose an attacker submits this as the username:
' OR '1'='1' -- -
The query the database actually receives becomes:
SELECT id, role FROM users WHERE username = '' OR '1'='1' -- -' AND password_hash = SHA2('', 256)
Two things happened. OR '1'='1' is always true, so the WHERE clause matches every row. And -- starts a SQL comment, so the password check is discarded entirely. If the handler treats any returned row as success, the attacker is authenticated under the first matching account — a choice that can turn query ordering into privilege selection.
The attacker never needed a valid password. They only needed the application to confuse their input with its own query logic.
Reading data that was never meant to be returned
Authentication bypass is just one possible effect. When a query's result is rendered, a UNION can append another compatible result set. In an authorized lab, a fixed marker is enough to demonstrate that control:
' UNION SELECT 'ssec-lab', 0 -- -
If the original query returned two compatible columns, ssec-lab appears where a product name was expected. That proves an injected result reached the page without selecting private data. A real attacker could replace the constants with expressions or rows the application database account is allowed to read.
When nothing is echoed back: blind SQLi
Many endpoints do not display query results directly. They still leak data through their behaviour. In boolean-based blind SQLi, the attacker asks true/false questions and watches how the page responds:
' AND SUBSTRING('ABC', 1, 1) = 'A' -- -
If the page renders normally, the fixed condition is true; pair it with = 'B' for a false control. This establishes a boolean oracle without reading a row. When even the response body is identical, a short conditional delay can provide a lab-safe timing proof:
' AND IF(1=1, SLEEP(2), 0) -- -
A repeated two-second true/false difference answers whether the condition executed. An authorized assessment should stop there; attackers can automate similar decisions against readable values.
Where SQL injection hides
Search boxes and login forms are the familiar examples, but the vulnerable value does not have to come from a visible text field. Sort direction, report columns, JSON properties, path segments, cookies, imported files and stored profile values can all reach a query. ORMs reduce routine string building, but raw-query methods and dynamically assembled HQL or LINQ expressions can reopen the same boundary.
The quickest review question is: can this value affect SQL structure, or is it bound only as data? Values used for table names, column names and sort keywords need special attention because most database APIs cannot bind identifiers. Map those choices from a small request value to a fixed server-owned identifier rather than copying the request into SQL.
Why it keeps happening
SQLi is not a database bug. It is a failure to separate code from data. Any place that concatenates input into a query is a candidate: login forms, search boxes, sort parameters, JSON API fields, even HTTP headers that get logged into a database. Blocklists and naive escaping miss edge cases across 19 database families, each with its own quoting rules and functions.
The fix: parameterized queries
The reliable defence is to never build queries by concatenation. Use parameterized queries (prepared statements) so the database treats input strictly as a value, never as syntax. For authentication, look up one account by a bound identifier and verify its stored password hash in application code:
In PHP with PDO:
$stmt = $db->prepare('SELECT id, role, password_hash FROM users WHERE username = ?'); $stmt->execute([$username]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if (!$user || !password_verify($password, $user['password_hash'])) { http_response_code(401); exit; }
With parameters, ' OR '1'='1' -- - is just a failed username lookup. There is no syntax to break out of. Password verification also uses the algorithm and work factor recorded in the stored hash. Pair this with least-privilege database accounts, narrowly scoped views and an ORM where practical.
Fixes that do not hold
- Blocking quotes or SQL words: encodings, alternate operators and database-specific syntax make blocklists incomplete.
- Escaping everything: escaping is context- and database-specific, and one later refactor can invalidate the assumption. OWASP treats it as a discouraged last resort.
- Hiding database errors: this removes one feedback channel but leaves boolean, time and out-of-band techniques available.
- Adding a WAF: a WAF can reduce exposure and buy response time, but it cannot restore the missing code/data boundary in the application.
- Assuming an ORM is automatically safe: raw fragments, dynamic identifiers and unsafe query APIs remain injectable.
A developer review checklist
- Search for query strings built with interpolation, concatenation or formatting.
- Check raw-query, stored-procedure and ORM escape hatches as carefully as direct SQL.
- Bind every scalar value with an explicit type.
- Map structural choices such as sort columns from an allowlist owned by the server.
- Give the application database account only the tables and operations it needs.
- Return generic database failures to clients while retaining safe server-side diagnostics.
References
How SelfSec proves this one
Scanner behaviour- Module
- SQL Injection sub-engine
- How it probes
- Fingerprints the database family first, then runs error, boolean-blind, time-based, union, stacked and out-of-band probes against each injectable parameter.
- How it confirms
- A per-target timing baseline built from the median and standard deviation rules out network jitter, and an out-of-band callback proves the blind cases the response body never shows.
Reported as CWE-89 · ATT&CK T1190 · SARIF 2.1.0
A reflected response alone is not enough to promote a SQL Injection finding — the classical engine has to confirm the behaviour before it reaches the report. See the detection engine
SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.
Do both things about SQL Injection
SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.