Side Channel Attack with Jwt Tokens
How Side Channel Attacks Manifest in JWT Tokens
Side channel attacks on JWT tokens exploit timing variations and information leakage through implementation details rather than breaking cryptographic algorithms directly. The most common JWT-specific side channel occurs during token verification when secret key comparisons are performed insecurely.
Consider this vulnerable JWT verification code:
const jwt = require('jsonwebtoken');
function verifyToken(token) {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
return { valid: true, payload };
} catch (err) {
return { valid: false, error: err.message };
}
}
// Attacker can measure response times
const startTime = Date.now();
const result = verifyToken('invalid.token.here');
const duration = Date.now() - startTime;
console.log(`Verification took ${duration}ms`);
The vulnerability lies in how the JWT library performs signature verification. When an attacker submits tokens with modified headers or payloads, the verification process takes different amounts of time depending on how much of the token was correct before failing. This timing difference reveals information about the secret key.
Another JWT-specific side channel involves the exp (expiration) claim. Many implementations check expiration before signature verification:
function vulnerableVerify(token) {
const [header, payload, sig] = token.split('.');
// Check expiration FIRST
const payloadData = JSON.parse(atob(payload));
if (payloadData.exp < Date.now() / 1000) {
return false; // Immediately fails expired tokens
}
// Only then verify signature
return crypto.timingSafeEqual(
Buffer.from(sig),
Buffer.from(computeSignature(header, payload, secret))
);
}
This ordering creates a timing oracle: expired tokens fail instantly, while valid-but-forged tokens take longer to process. An attacker can distinguish between 'expired' and 'invalid signature' responses based on response time, then focus attacks on non-expired tokens.
Algorithm confusion attacks also create side channels. When JWT libraries support the 'none' algorithm or allow algorithm switching, attackers can craft tokens that trigger different verification paths:
function algorithmConfusion(token) {
const [headerB64] = token.split('.');
const header = JSON.parse(atob(headerB64));
// Different code paths based on algorithm
if (header.alg === 'HS256') {
// Symmetric verification - slower
return verifyHS256(token);
} else if (header.alg === 'none') {
// No verification - instant
return true;
} else if (header.alg === 'RS256') {
// Asymmetric verification - different timing
return verifyRS256(token);
}
}
The varying execution paths create measurable timing differences that attackers can exploit to determine which verification branch was taken, potentially bypassing security controls.
JWT-Specific Detection Methods
Detecting JWT side channel vulnerabilities requires both static analysis and runtime testing. Start with code review focusing on these patterns:
const vulnerablePatterns = [
// Insecure string comparison
/===\s*\w+\.split\(\)\[2\]/,
// Early expiration check
/if\s*\(.*exp.*<.*Date\.now/i,
// Algorithm confusion
/header\.alg\s*===\s*['"]none['"]/i,
// Missing constant-time comparison
/crypto\.timingSafeEqual/i
];
Runtime detection involves measuring verification times across controlled inputs. A simple timing oracle detector:
async function detectTimingOracle(endpoint, tokenTemplate) {
const measurements = [];
// Test with valid-looking token
for (let i = 0; i < 10; i++) {
const modifiedToken = modifyTokenHeader(tokenTemplate, i);
const start = process.hrtime.bigint();
const result = await verifyViaAPI(endpoint, modifiedToken);
const duration = Number(process.hrtime.bigint() - start) / 1000000;
measurements.push({ token: modifiedToken, duration, result });
}
// Analyze for timing patterns
const sorted = measurements.sort((a, b) => a.duration - b.duration);
const variance = sorted[sorted.length - 1].duration - sorted[0].duration;
if (variance > 5) { // More than 5ms difference
return {
vulnerable: true,
maxVariance: variance,
suspiciousTokens: sorted.slice(0, 3)
};
}
return { vulnerable: false };
}
Automated scanning tools like middleBrick can detect these vulnerabilities by:
- Submitting JWT tokens with controlled modifications and measuring response time variance
- Analyzing code for insecure comparison patterns and early validation checks
- Testing algorithm confusion scenarios with crafted tokens
- Scanning for exposed JWT endpoints that don't implement constant-time operations
middleBrick's black-box scanning approach tests the unauthenticated attack surface, submitting tokens with timing variations and analyzing response patterns without requiring source code access. The scanner identifies endpoints vulnerable to timing attacks by measuring the statistical significance of response time differences across multiple test cases.
JWT-Specific Remediation Techniques
Fixing JWT side channel vulnerabilities requires implementing constant-time operations and proper verification ordering. Here's the secure approach:
// Secure JWT verification with constant-time operations
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
function secureVerify(token, secret) {
try {
// Step 1: Always perform constant-time comparison
const [headerB64, payloadB64, sig] = token.split('.');
const expectedSig = computeConstantTimeSignature(headerB64, payloadB64, secret);
// Step 2: Verify signature FIRST using timing-safe comparison
const sigBuffer = Buffer.from(sig, 'base64');
const expectedBuffer = Buffer.from(expectedSig, 'base64');
if (!crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
return { valid: false, reason: 'invalid_signature' };
}
// Step 3: Only then check expiration and other claims
const payload = JSON.parse(Buffer.from(payloadB64, 'base64').toString());
if (payload.exp < Date.now() / 1000) {
return { valid: false, reason: 'expired' };
}
return { valid: true, payload };
} catch (err) {
return { valid: false, reason: 'malformed' };
}
}
function computeConstantTimeSignature(header, payload, secret) {
// Always perform the same operations regardless of input
const message = `${header}.${payload}`;
const hmac = crypto.createHmac('sha256', secret);
hmac.update(message);
return hmac.digest('base64');
}
For applications using JWT libraries, ensure they implement constant-time verification internally. With the jsonwebtoken library:
const jwt = require('jsonwebtoken');
// Always use the same verification path
function verifyWithFixedTiming(token, secret) {
// Add a constant delay to mask timing variations
const startTime = process.hrtime.bigint();
try {
const payload = jwt.verify(token, secret, {
algorithms: ['HS256'], // Never allow 'none'
complete: true
});
// Constant delay to normalize timing
const targetDuration = BigInt(10000000); // 10ms
const elapsed = process.hrtime.bigint() - startTime;
if (elapsed < targetDuration) {
const sleepTime = targetDuration - elapsed;
await new Promise(resolve => setTimeout(resolve, Number(sleepTime / 1000000n)));
}
return { valid: true, payload };
} catch (err) {
// Same delay for failures
const targetDuration = BigInt(10000000);
const elapsed = process.hrtime.bigint() - startTime;
if (elapsed < targetDuration) {
const sleepTime = targetDuration - elapsed;
await new Promise(resolve => setTimeout(resolve, Number(sleepTime / 1000000n)));
}
return { valid: false, reason: err.message };
}
}
Additional mitigations include:
| Mitigation | Implementation | Effectiveness |
|---|---|---|
| Constant-time comparison | crypto.timingSafeEqual | High - eliminates timing differences |
| Fixed verification order | Signature → Claims → Response | High - prevents early exits |
| Uniform response times | Artificial delays for all outcomes | Medium - adds overhead |
| Algorithm whitelisting | Explicit algorithms: ['HS256'] | High - prevents confusion |
middleBrick's remediation guidance includes specific code snippets for each vulnerability type, mapping findings to OWASP API Security Top 10 controls and providing framework-specific fixes for Node.js, Python, and Java implementations.