Engineering · Aug 5, 2026 · 7 min read

Blind SQL injection: boolean and time oracles when the error is gone

Production databases rarely leak error messages. A boolean oracle that flips a conditional and a time oracle that delays the response confirm injection without a single visible error.

Production databases rarely print their errors to HTTP responses. A scanner that stops when there is nothing to read in the body misses most SQL injection surfaces. The confirming technique is behavioral: change what the database computes, then measure what the application returns - no error message needed, no data touched.

Proven - HighBoolean SQLi confirmed - /api/products?category (integer param)
baseline GET /api/products?category=4 ──► 200 1842 B 3 rows true-arm GET /api/products?category=4 AND 62615533=62615533-- ──► 200 1842 B (matches) false-arm GET /api/products?category=4 AND 62615533=62615534-- ──► 200 94 B 0 rows delta 1748 B across 3 independent repeats - no error, no write, no read.
Oracle: boolean differential (content-length). Rated high pending exfil confirmation. Arithmetic nonce 62615533 avoids WAF keyword matching. Fix: parameterized query at the category filter.
2
oracle families
0
rows read or written during probe
3x
repeats per arm to beat jitter

01What an oracle is and why it works here

SQL boolean conditions control which rows the WHERE clause returns. Injecting AND 1=1 into a numeric parameter keeps the original semantics intact. Injecting AND 1=2 makes the condition false - the WHERE clause eliminates every row. If the application's response changes between those two payloads, user input is being interpreted as SQL rather than treated as a quoted string. That behavioral difference is the artifact - not an error message, not a data read, just a measurable response shift that proves the boundary is open.

The same logic drives time oracles. A conditional sleep - AND IF(1=1,SLEEP(3),0) - evaluates only when the boolean arm is true. If the true arm adds three seconds and the false arm does not, the database ran the sleep, which means it evaluated the injected condition. Response shape is irrelevant; only latency is measured.

Boolean path user input application builds query database category=4 ──► WHERE category = 4 ──► 3 rows 200 OK 1842 B category=4 AND 1=1-- ──► WHERE category = 4 AND 1=1 ──► 3 rows 200 OK 1842 B category=4 AND 1=2-- ──► WHERE category = 4 AND 1=2 ──► 0 rows 200 OK 94 B Time path (time oracle fallback when body is constant) user_id=42 ──► 91 ms no delay user_id=42; IF(1=2) WAITFOR DELAY '0:0:3'-- ──► 93 ms no delay user_id=42; IF(1=1) WAITFOR DELAY '0:0:3'-- ──► 3094 ms DELAY FIRED
Boolean path: content-length delta is the artifact. Time path: latency delta when body gives nothing away.

02Running the boolean probe step by step

Three requests, in order. First, the baseline - record the exact response size and field count for the unmodified parameter. Second, the true arm - the arithmetic nonce 62615533=62615533 must return a response indistinguishable from the baseline. Third, the false arm - 62615533=62615534 must return something measurably different. The confirming artifact is the delta between arm two and arm three, held across three independent repeats to rule out cache variance.

boolean-probe.httphttp
# 1. Baseline - record Content-Length
GET /api/products?category=4
#    200  Content-Length: 1842  (3 products)

# 2. True arm - must match baseline exactly
GET /api/products?category=4 AND 62615533=62615533--
#    200  Content-Length: 1842  OK - matches

# 3. False arm - collapses if input is interpreted as SQL
GET /api/products?category=4 AND 62615533=62615534--
#    200  Content-Length:   94  DIFFERENT - boolean injection live

# Repeat false arm 2 more times to confirm not a cache fluke.

03Time oracle: the fallback when content is constant

Some endpoints return the same response shape regardless of the WHERE result - a wrapper that always returns an empty list, a count stored procedure, a cached view. Boolean content deltas vanish. The only observable channel left is time. A conditional sleep fires only when the injected boolean arm evaluates to true, and the latency delta is the artifact. Three seconds is the practical sleep value: long enough to clear application jitter, short enough to stay under most CDN and load-balancer timeouts. Take a baseline average over three clean requests first.

DatabaseSleep functionSafe conditional formNotes
MySQL / MariaDBSLEEP(3)AND IF(1=1,SLEEP(3),0)--IF() evaluates only one arm
PostgreSQLpg_sleep(3)AND (SELECT CASE WHEN 1=1 THEN pg_sleep(3) END) IS NOT NULL--CASE prevents eager eval
MSSQLWAITFOR DELAY '0:0:3'; IF (1=1) WAITFOR DELAY '0:0:3'--Stacked statement - needs stacking allowed
Oracledbms_pipe.receive_messageAND 1=(SELECT CASE WHEN 1=1 THEN dbms_pipe.receive_message(CHR(65),3) END FROM dual)--Requires execute privilege on dbms_pipe
time-probe-mssql.httphttp
# Baseline (x3 avg)
GET /report?user_id=42
#    avg 91 ms

# False arm - WAITFOR must NOT fire (1=2 is false)
GET /report?user_id=42; IF (1=2) WAITFOR DELAY '0:0:3'--
#    avg 93 ms   no delay

# True arm - WAITFOR fires if injection is live
GET /report?user_id=42; IF (1=1) WAITFOR DELAY '0:0:3'--
#    avg 3094 ms  DELAY FIRED - injection confirmed
Proven - HighTime-based SQLi - MSSQL WAITFOR DELAY - /report?user_id
baseline user_id=42 91 ms false arm user_id=42; IF (1=2) WAITFOR DELAY '0:0:3' 93 ms (no delay) true arm user_id=42; IF (1=1) WAITFOR DELAY '0:0:3' 3094 ms (delay fired x3)
Oracle: time-differential (WAITFOR DELAY, MSSQL). Zero reads and zero writes during confirmation. Rated high - stacked query context; escalation to critical if xp_cmdshell or bulk-read capability is confirmed.

04Severity is the injection class, not the oracle

Both oracles prove the same root cause: unparameterized input reaches a SQL interpreter. What the interpreter can then do decides the final severity. A boolean or time confirmation is the floor. Escalation requires demonstrating the highest capability the injection context permits - a UNION-based data read, a stacked write, or an OS-command path - each documented in the finding as the escalation path without being demonstrated by a destructive payload during the probe itself.

The fix is consistent across both oracle types and all four databases: parameterized queries, everywhere user input touches SQL. Escaping is not a substitute - every well-documented bypass shows that the quoting layer and the parser diverge under charset switches, multi-byte encodings, and second-order injection contexts. ORM raw-query escape hatches and string-concatenation helpers inside query builders are parameterization gaps even when the ORM is otherwise used correctly. Each finding carries the parameter name, the injection context, both probe payloads, and the measured delta - enough to locate the unbound query and replace it.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →