Timing Attack with Jwt Tokens
How Timing Attack Manifests in JWT Tokens
JSON Web Token (JWT) verification typically involves computing a cryptographic signature (e.g., HMAC‑SHA256) over the header and payload and then comparing it to the signature supplied in the token. If the comparison is performed with a naive equality operator (such as == in JavaScript or != in Python), the comparison stops at the first mismatching byte. This early‑exit creates a measurable timing difference that depends on how many leading bytes of the two signatures match.
An attacker can exploit this by sending many tokens that differ only in a single byte of the signature and measuring the server’s response time. By observing which guess yields a slightly longer processing time, the attacker can infer the correct byte, then move to the next position. Repeating this process across the entire signature (usually 32 bytes for HS256) allows the attacker to forge a valid token without knowing the secret key.
The vulnerable code path appears wherever a developer implements manual signature verification instead of relying on a battle‑tested library. Typical examples include:
- Node.js:
if (computedSig !== receivedSig) { return res.status(401).send('Invalid'); } - Python:
if computed_sig != received_sig: raise InvalidSignature - Any custom middleware that decodes the token, recomputes the HMAC, and uses a simple string compare.
Even when using a library, outdated versions may contain the same flaw. For instance, PyJWT versions prior to 1.5.0 used an insecure comparison for the HMAC check (CVE‑2016‑10555), making them susceptible to timing attacks.
JWT Tokens‑Specific Detection
middleBrick detects timing discrepancies in JWT verification by launching a series of probing requests against the target endpoint. Each request contains a JWT where the signature byte at a chosen position is varied while the rest of the token remains constant. The scanner measures the response time for each variant with high‑resolution timers and applies statistical tests (e.g., t‑test) to identify statistically significant differences.
Because the scan is unauthenticated and black‑box, it does not need any credentials or configuration. The user simply supplies the API URL (e.g., https://api.example.com/auth/validate) and middleBrick runs the following steps:
- Extract any JWT‑accepting endpoint from the OpenAPI spec (if provided) or discover it via common paths.
- Generate a baseline token with a known invalid signature.
- For each byte position in the signature, create 256 variants (one for each possible byte value) and record the latency.
- If a clear timing leak is found, the scanner reports a finding under the "Authentication" category with severity "high", includes the exact byte position that leaked, and provides remediation guidance.
The same detection logic is available through the middleBrick CLI (middlebrick scan https://api.example.com/auth/validate), the GitHub Action (which can fail a build when a timing‑attack finding appears), and the MCP Server for scanning directly from an IDE.
JWT Tokens‑Specific Remediation
The fix is to replace any non‑constant‑time comparison with a cryptographically constant‑time function. Most modern JWT libraries already do this, so the simplest remedy is to keep your dependencies up‑to‑date. When manual verification is unavoidable, use the language‑specific constant‑time primitives shown below.
Node.js (using the built‑in crypto module)
const crypto = require('crypto');
function verifyToken(token, secret) {
const [encodedHeader, encodedPayload, receivedSig] = token.split('.');
const signed = `${encodedHeader}.${encodedPayload}`;
const computedSig = crypto.createHmac('sha256', secret)
.update(signed, 'utf8')
.digest('base64url');
// Constant‑time comparison
const match = crypto.timingSafeEqual(
Buffer.from(computedSig, 'utf8'),
Buffer.from(receivedSig, 'utf8')
);
if (!match) {
throw new Error('Invalid signature');
}
// … proceed with decoding payload …
}
Python (using hmac)
import hmac
import hashlib
import base64
def verify_token(token: str, secret: bytes) -> bool:
try:
header_b64, payload_b64, sig_b64 = token.split('.')
signing_input = f'{header_b64}.{payload_b64}'.encode()
mac = hmac.new(secret, signing_input, hashlib.sha256)
expected_sig = base64.urlsafe_b64encode(mac.digest()).rstrip(b'=')
# Constant‑time compare
return hmac.compare_digest(expected_sig, sig_b64.encode())
except Exception:
return False
# Usage
if not verify_token(token, b'your‑256‑bit‑secret'):
raise HTTPException(status_code=401, detail='Invalid token')
Additional hardening steps:
- Always verify the
algfield in the JWT header; reject tokens with alg="none". - Prefer using well‑maintained libraries (e.g.,
jsonwebtokenfor Node.js,PyJWT>=1.5.0for Python,firebase-adminfor Java) which already implement constant‑time verification. - Deploy the API behind a TLS termination layer (HTTPS) to prevent network noise, but remember that TLS does not eliminate the timing side‑channel.
- Enable request‑level rate limiting to slow down brute‑force timing attacks; middleBrick’s Rate Limiting check can help verify this control is present.