Timing Attack with Bearer Tokens
How Timing Attack Manifests in Bearer Tokens
When an API validates a Bearer token, many developers compare the supplied token with the expected value using a simple equality check (e.g., === in JavaScript, == in Python, or .equals() in Java). These operators short‑circuit: they stop comparing as soon as they find a mismatching character. An attacker who can measure the response time of successive guesses can infer how many leading characters matched, gradually rebuilding a valid token. This is a classic timing side‑channel that targets the authentication step of Bearer token handling.
Typical vulnerable code paths look like this:
// Node.js/Express – vulnerable
app.get('/protected', (req, res) => {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
return res.status(401).send('Missing token');
}
const token = auth.substring(7);
if (token === process.env.API_TOKEN) { // ← non‑constant‑time compare
return res.send('OK');
}
return res.status(403).send('Invalid token');
});
The same pattern appears in Python Flask, Java Spring, and Go net/http handlers when the token is compared with ==, equals, or simple slice equality.
Bearer Tokens-Specific Detection
Detecting a timing leak in Bearer token validation requires observing how response time changes with different token prefixes. middleBrick’s unauthenticated black‑box scan includes a timing‑check module that:
- Sends a series of Bearer tokens that share a common prefix and differ in the next character (e.g.,
Bearer AAAAx…,Bearer AAABy…, …). - Measures the latency of each request with high‑resolution timers.
- Looks for statistically significant differences that indicate a short‑circuit comparison.
- Flags the endpoint when the variance exceeds a threshold derived from baseline noise.
Because the scan works without any agents or credentials, it can be run against a staging or production URL simply by pasting the endpoint into the middleBrick dashboard, CLI, or GitHub Action. The resulting report lists the finding under the “Authentication” category, includes the measured timing delta, and provides remediation guidance.
Example of a middleBrick CLI trigger:
middlebrick scan https://api.example.com/resource --output json
The JSON output will contain a field like:
{
"check": "timing_attack_bearer_token",
"severity": "medium",
"description": "Token comparison uses non‑constant‑time equality, enabling timing side‑channel.",
"remediation": "Use constant‑time comparison functions such as crypto.timingSafeEqual (Node.js) or hmac.compare_digest (Python)."
}Bearer Tokens-Specific Remediation
Fixing the issue means replacing the naïve equality with a constant‑time comparison that always runs in the same amount of time regardless of where the first mismatch occurs.
Node.js (crypto.timingSafeEqual):
const crypto = require('crypto');
app.get('/protected', (req, res) => {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
return res.status(401).send('Missing token');
}
const token = Buffer.from(auth.substring(7), 'utf8');
const expected = Buffer.from(process.env.API_TOKEN, 'utf8');
if (crypto.timingSafeEqual(token, expected)) {
return res.send('OK');
}
return res.status(403).send('Invalid token');
});
Python (hmac.compare_digest):
import hmac
from flask import Flask, request, Response
app = Flask(__name__)
API_TOKEN = b's3cr3t'
@app.route('/protected')
def protected():
auth = request.headers.get('Authorization')
if not auth or not auth.startswith('Bearer '):
return Response('Missing token', status=401)
token = auth[7:].encode('utf-8')
if hmac.compare_digest(token, API_TOKEN):
return Response('OK')
return Response('Invalid token', status=403)
Java (using Bouncy Castle’s constant‑time array comparison):
import org.bouncycastle.util.Arrays;
boolean constantTimeEquals(byte[] a, byte[] b) {
return Arrays.constantTimeAreEqual(a, b);
}
// In a Spring filter
String auth = request.getHeader("Authorization");
if (auth == null || !auth.startsWith("Bearer ")) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing token");
return;
}
String tokenStr = auth.substring(7);
if (!constantTimeEquals(tokenStr.getBytes(StandardCharsets.UTF_8),
EXPECTED_TOKEN.getBytes(StandardCharsets.UTF_8))) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid token");
return;
}
// proceed
By using these constant‑time primitives, the comparison no longer leaks timing information, and the Bearer token validation step becomes resistant to this side‑channel.