3 minute read

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:

  1. Open an HTTP/2 connection to the target
  2. 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)
  3. Measure which response arrives first
  4. If A arrives before B, the server spent less time on A (early rejection = wrong character)
  5. 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:

  1. Open N TCP connections (pre-handshake)
  2. Send the timing request on connection 1 and baseline on connection 2 in rapid succession
  3. Use the TCP SYN-ACK timing to establish a baseline for network conditions
  4. Repeat many times and use statistical analysis

This is less precise than HTTP/2 multiplexing but still significantly better than sequential requests.

Defenses

  1. Constant-time comparison functions: Use hmac.compare_digest() in Python, crypto.timingSafeEqual() in Node.js, hash_equals() in PHP
  2. Artificial delays: Add random sleep before responding (reduces throughput, does not eliminate the side channel)
  3. Rate limiting: Limit authentication attempts per time window
  4. 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)