Practical Timeless Timing Attacks
Timing attacks on web applications have traditionally been unreliable due to network jitter. A 1ms difference in server processing time can be obscured by 50ms of network variance. In this post, I explore practical techniques for exploiting timing side-channels in web applications using timeless timing attacks.
The Problem with Traditional Timing Attacks
A classic timing attack measures response time to infer information:
import requests, time
for char in 'abcdefghijklmnopqrstuvwxyz':
start = time.time()
requests.get(f'https://target.com/api?token={char}...')
elapsed = time.time() - start
print(f'{char}: {elapsed:.4f}s')
The vulnerability exists when the server performs byte-by-byte comparison:
# Vulnerable: timing leaks character position
def check_token(provided, actual):
for i in range(len(actual)):
if provided[i] != actual[i]:
return False
return True
But network latency between attacker and server is typically 10-200ms with 5-50ms jitter, while the timing difference per character is often less than 1ms.
Timeless Timing via HTTP/2 Multiplexing
HTTP/2 multiplexes requests over a single TCP connection. If we send two requests simultaneously on the same connection, they arrive at the server at the same time, and the difference in response times reflects only server-side processing - network jitter is eliminated.
The technique:
- Open an HTTP/2 connection to the target
- Send two requests in the same TCP segment:
- Request A: the timing oracle (e.g., login with guessed token prefix)
- Request B: a baseline request with known timing (e.g., login with empty token)
- Measure which response arrives first
- If A arrives before B, the server spent less time on A (early rejection = wrong character)
- If B arrives before A, the server spent more time on A (later rejection = correct prefix)
import h2.connection
import h2.events
import socket, ssl
# Establish HTTP/2 connection
ctx = ssl.create_default_context()
ctx.set_alpn_protocols(['h2'])
sock = socket.create_connection(('target.com', 443))
sock = ctx.wrap_socket(sock, server_hostname='target.com')
conn = h2.connection.H2Connection()
conn.initiate_connection()
sock.sendall(conn.data_to_send())
# Send two requests in same TCP write
stream_a = conn.get_next_available_stream_id()
conn.send_headers(stream_a, headers_a)
conn.send_data(stream_a, body_a)
stream_b = conn.get_next_available_stream_id()
conn.send_headers(stream_b, headers_b)
conn.send_data(stream_b, body_b)
# Critical: send both in one TCP segment
sock.sendall(conn.data_to_send())
# Observe which stream completes first
Practical Results
I tested this technique against several real-world scenarios:
Token Brute-Force
Target: a custom API with byte-by-byte token comparison.
- Traditional timing: unable to distinguish with 95% confidence
- Timeless timing: 99.7% accuracy after 20 repetitions per character
- Total time to extract 32-character token: approximately 15 minutes
Username Enumeration
Target: a login form with different code paths for valid/invalid usernames.
- Timing difference: approximately 2ms (database lookup for valid users)
- Traditional timing: 60% accuracy (barely above random)
- Timeless timing: 94% accuracy after 50 repetitions
Password Hash Comparison
Target: bcrypt comparison where early-exit on wrong hash is detectable.
- This is a harder target because bcrypt is intentionally slow (100ms+)
- The timing difference is in the comparison after hashing, typically <0.1ms
- Timeless timing: detectable but requires 200+ repetitions per bit
Beyond HTTP/2: TCP Concurrent Requests
For HTTP/1.1 targets, a similar technique works using multiple TCP connections initiated simultaneously:
- Open N TCP connections (pre-handshake)
- Send the timing request on connection 1 and baseline on connection 2 in rapid succession
- Use the TCP SYN-ACK timing to establish a baseline for network conditions
- Repeat many times and use statistical analysis
This is less precise than HTTP/2 multiplexing but still significantly better than sequential requests.
Defenses
- Constant-time comparison functions: Use
hmac.compare_digest()in Python,crypto.timingSafeEqual()in Node.js,hash_equals()in PHP - Artificial delays: Add random sleep before responding (reduces throughput, does not eliminate the side channel)
- Rate limiting: Limit authentication attempts per time window
- HTTP/2 request queuing: Process multiplexed requests sequentially (breaks timeless timing but reduces HTTP/2 performance benefits)
Conclusion
Timeless timing attacks make previously impractical timing side-channels exploitable. Any server-side comparison that is not constant-time should be considered vulnerable when the server supports HTTP/2 or when the attacker can establish concurrent connections.
References
- Tom Van Goethem et al., “Timeless Timing Attacks” (USENIX Security 2020)
- Paul Bischoff, “HTTP/2 Rapid Reset Attack”
- Daniel J. Bernstein, “Cache-timing attacks on AES” (2005)