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.
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.
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.
# 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.
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.
| Database | Sleep function | Safe conditional form | Notes |
|---|---|---|---|
| MySQL / MariaDB | SLEEP(3) | AND IF(1=1,SLEEP(3),0)-- | IF() evaluates only one arm |
| PostgreSQL | pg_sleep(3) | AND (SELECT CASE WHEN 1=1 THEN pg_sleep(3) END) IS NOT NULL-- | CASE prevents eager eval |
| MSSQL | WAITFOR DELAY '0:0:3' | ; IF (1=1) WAITFOR DELAY '0:0:3'-- | Stacked statement - needs stacking allowed |
| Oracle | dbms_pipe.receive_message | AND 1=(SELECT CASE WHEN 1=1 THEN dbms_pipe.receive_message(CHR(65),3) END FROM dual)-- | Requires execute privilege on dbms_pipe |
# 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
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.