Menu: TECH OPS > IT Security
Article Type: Security Advisory
Executive Summary
A high-severity authentication bypass vulnerability exists in the fast-jwt library when caching is enabled. The verifier fails to properly enforce token expiration (exp claim) for JWTs that lack an iat (issued-at) claim. Expired tokens without iat continue to be accepted from the cache for up to 10 minutes after their official expiration time, allowing attackers to extend compromised sessions and gain unauthorized access.
Impact: Session hijacking, unauthorized API access, and circumvention of token-based access controls.
Affected Component: fast-jwt library, src/verifier.js with cache: true configuration.
Recommendation: Immediately audit your codebase for fast-jwt usage, disable caching or upgrade to a patched version, and ensure all token issuers include the iat claim.
What Is the fast-jwt Cache Expiration Bypass?
How the Vulnerability Works
The fast-jwt library implements a caching layer to improve performance by avoiding repeated cryptographic verification of the same JWT. However, when determining how long a cached token should remain valid, the library makes a critical assumption: it only calculates the cache deadline based on the token’s exp claim if the token also contains an iat claim.
When a JWT lacks an iat claim—which is optional per the JWT specification—the cache deadline calculation is skipped entirely. Instead, the library falls back to a generic 10-minute cache TTL. This means an expired token without iat will continue to be served as valid from the cache for up to 10 minutes after its official expiration time.
Why Caching Creates the Security Gap
The vulnerability exists because of how fast-jwt structures its verification pipeline:
- Cache Lookup (First): When a token is presented, fast-jwt first checks if it exists in the cache.
- Early Return: If found and the cache deadline hasn’t passed, the cached payload is returned immediately.
- Expiration Check (Skipped): The
verifyToken()function—which validates theexpclaim—is never executed on cache hits.
This optimization is sound in principle, but only if the cache deadline accurately reflects the token’s actual expiration. When iat is missing, the deadline is set incorrectly, creating a window where expired tokens bypass validation.
Real-World Attack Timeline
T=0s: User logs in. JWT issued with exp=1700000001, no iat claim.
T=0s: Token verified and cached. Cache deadline set to T+600s (10 min fallback).
T=300s: Token still valid. User makes API request. Served from cache.
T=600s: Token officially expires (exp timestamp reached).
T=605s: Attacker reuses expired token. Cache deadline (T+600s) hasn't passed.
T=605s: fast-jwt returns cached payload without calling verifyToken().
T=605s: Attacker gains unauthorized access with expired credentials.
T+600s: Cache entry finally expires. Subsequent requests are rejected.
Affected Systems & Scope
Vulnerable Library and Component
- Library:
fast-jwt(npm package) - Component:
src/verifier.js(Verifier module) - Configuration: Vulnerability only manifests when
cache: trueis set
All Affected Versions
All versions of fast-jwt containing the vulnerable cacheSet and cache lookup logic in src/verifier.js where exp cache deadline derivation is conditional on the presence of an iat claim.
Configuration Requirements for Exploitation
The vulnerability requires all of the following conditions:
- Application uses
fast-jwtwithcache: trueenabled - JWT issuer produces tokens with
expclaim but withoutiatclaim - Token is verified and cached at least once before expiration
- Attacker reuses the same token after its official expiration time
- Reuse occurs within the cache TTL window (default: 10 minutes)
Technical Root Cause Analysis
The Conditional iat Claim Check
The vulnerability is located in src/verifier.js at lines 127–140:
const hasIat = payload && typeof payload.iat === 'number'
if (hasIat) {
if (!ignoreExpiration && typeof payload.exp === 'number') {
cacheValue[2] = payload.exp * 1000 + clockTolerance
}
}
The Problem: The exp cache deadline is only calculated inside the if (hasIat) block. If iat is missing, this entire block is skipped, leaving cacheValue[2] (the cache expiration deadline) set to 0.
Cache Deadline Calculation Flaw
When cacheValue[2] remains 0, the fallback logic at lines 142–146 applies:
const maxTTL = clockTimestamp + clockTolerance + cacheTTL
cacheValue[2] = cacheValue[2] === 0 ? maxTTL : Math.min(cacheValue[2], maxTTL)
This assigns a generic TTL (default 10 minutes) instead of the token’s actual expiration time. The cache entry is now valid for 10 minutes from the current time, regardless of when the token actually expires.
How Expired Tokens Bypass Verification
On subsequent requests, the cache lookup at lines 372–388 executes:
const [value, min, max] = cache.get(cacheKeyBuilder(token)) || [undefined, 0, 0]
if (typeof value !== 'undefined' && (max === 0 || now < max)) {
return value // Return cached payload WITHOUT calling verifyToken()
}
The condition now < max evaluates to true as long as the current time is before the cache deadline. Since the deadline was set to now + 10 minutes (not the token’s actual exp), the cached payload is returned even after the token has officially expired.
Attack Scenarios & Business Impact
Post-Expiration Session Extension Attack
Scenario: An e-commerce platform uses fast-jwt to verify bearer tokens for API requests.
- Initial Login: User authenticates and receives a JWT with
exp: 1700000001(1 hour validity) and noiatclaim. - First Request: User makes an API call. Token is verified, cached, and cache deadline is set to
now + 10 minutes. - Token Expiration: 1 hour passes. The token’s
exptimestamp is now in the past. - Attacker Reuse: An attacker who captured the token reuses it within the 10-minute cache window.
- Unauthorized Access: The API returns the cached payload without re-validating expiration. The attacker gains access to the user’s account.
Business Impact:
- Unauthorized transactions or data access
- Account takeover during the cache window
- Compliance violations (PCI-DSS, HIPAA, GDPR)
- Reputational damage
Unauthorized Access Window Duration
- Default Cache TTL: 10 minutes
- Vulnerability Window: From token expiration until cache entry expires
- Worst Case: An attacker can extend a compromised session by up to 10 minutes beyond its intended lifetime
Confidentiality and Integrity Risks
- Confidentiality: Attackers can read sensitive data (PII, financial records, health information) using expired tokens.
- Integrity: Attackers can modify data, perform unauthorized transactions, or change account settings.
- Availability: Attackers can exhaust rate limits or perform denial-of-service attacks using stolen tokens.
Threat Assessment for Your Organization
Severity Rating and CVSS Context
CVSS v3.1 Base Score: 7.5 (High)
- Attack Vector: Network
- Attack Complexity: Low
- Privileges Required: None
- User Interaction: None
- Scope: Unchanged
- Confidentiality Impact: High
- Integrity Impact: High
- Availability Impact: None
Rationale: An unauthenticated attacker can bypass token expiration validation with no special privileges or user interaction, leading to unauthorized access to protected resources.
Prerequisites for Exploitation
- Attacker must obtain a valid JWT (via network sniffing, XSS, CSRF, or social engineering)
- Token must lack an
iatclaim (attacker cannot control this; depends on issuer configuration) - Application must use fast-jwt with
cache: true(attacker cannot control this; depends on deployment) - Attacker must reuse token within cache TTL window (default 10 minutes)
Attack Vector and Network Exposure
- Vector: HTTP Bearer Token in Authorization header
- Exposure: Any API endpoint protected by fast-jwt verification
- Detectability: Difficult to detect without logging cache hits vs. full verifications
How to Detect the Vulnerability
Static Code Audit Checklist
Step 1: Identify fast-jwt Usage
Search your codebase for fast-jwt imports and verifier initialization:
grep -r "fast-jwt" package.json
grep -r "createVerifier" src/
grep -r "require.*fast-jwt" .
Step 2: Check Cache Configuration
For each createVerifier() call, verify the configuration:
// VULNERABLE: cache enabled
const verifier = createVerifier({ key: 'secret', cache: true })
// SAFE: cache disabled
const verifier = createVerifier({ key: 'secret', cache: false })
// SAFE: cache not specified (defaults to false)
const verifier = createVerifier({ key: 'secret' })
Step 3: Audit Token Issuance
Examine your JWT issuer to check if iat is included:
// VULNERABLE: missing iat
const token = jwt.sign({ sub: 'user123', exp: Math.floor(Date.now() / 1000) + 3600 }, secret)
// SAFE: includes iat
const token = jwt.sign({
sub: 'user123',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600
}, secret)
Dynamic Reproduction Test Script
Create a test file to verify vulnerability in your environment:
const { createVerifier } = require('fast-jwt')
const assert = require('assert')
// Create a vulnerable verifier (cache enabled)
const verifier = createVerifier({
key: 'test-secret',
cache: true,
cacheTTL: 600000 // 10 minutes in milliseconds
})
// Manually create a token with exp but NO iat
const jwt = require('jsonwebtoken')
const now = Math.floor(Date.now() / 1000)
const expiredToken = jwt.sign(
{ sub: 'alice', exp: now - 1 }, // Already expired
'test-secret',
{ noTimestamp: true } // Prevents automatic iat injection
)
console.log('Testing fast-jwt cache expiration bypass...
')
try {
// This should fail because token is expired
const result = verifier(expiredToken)
console.log('❌ VULNERABLE: Expired token accepted from cache')
console.log(' Payload:', result)
} catch (err) {
if (err.message.includes('FAST_JWT_EXPIRED')) {
console.log('✅ SAFE: Expired token correctly rejected')
} else {
console.log('⚠️ UNEXPECTED ERROR:', err.message)
}
}
Expected Output:
- Vulnerable: “Expired token accepted from cache”
- Safe: “Expired token correctly rejected”
Identifying Vulnerable Configurations in Production
Log Analysis:
Enable debug logging in fast-jwt to identify cache hits:
const verifier = createVerifier({
key: 'secret',
cache: true,
onCache: (token) => console.log('Cache hit:', token.sub) // Custom logging
})
Metrics to Monitor:
- Ratio of cache hits to full verifications
- Tokens accepted after their
exptimestamp - Requests using tokens without
iatclaim

Immediate Mitigation Actions
Disable Caching in fast-jwt Verifiers
Quickest Fix: Remove or set cache: false in all verifier configurations.
// BEFORE (Vulnerable)
const verifier = createVerifier({
key: process.env.JWT_SECRET,
cache: true
})
// AFTER (Safe)
const verifier = createVerifier({
key: process.env.JWT_SECRET,
cache: false // Disable caching
})
Impact: Slight performance reduction (cryptographic verification on every request), but eliminates the vulnerability entirely.
Timeline: Deploy immediately (no code changes required beyond configuration).
Enforce iat Claim in Token Issuance
Secondary Fix: Ensure all JWT issuers include the iat claim.
// Token issuer configuration
const token = jwt.sign(
{
sub: userId,
iat: Math.floor(Date.now() / 1000), // Add this line
exp: Math.floor(Date.now() / 1000) + 3600
},
process.env.JWT_SECRET
)
Impact: Minimal performance impact; ensures cache deadline is calculated correctly.
Timeline: Update token issuers within 24 hours.
Apply Source Code Patch to src/verifier.js
Permanent Fix: Uncouple exp calculation from the hasIat check.
Original Code (Lines 127–146):
const hasIat = payload && typeof payload.iat === 'number'
if (hasIat) {
if (!ignoreExpiration && typeof payload.exp === 'number') {
cacheValue[2] = payload.exp * 1000 + clockTolerance
}
}
const maxTTL = clockTimestamp + clockTolerance + cacheTTL
cacheValue[2] = cacheValue[2] === 0 ? maxTTL : Math.min(cacheValue[2], maxTTL)
Patched Code:
const hasIat = payload && typeof payload.iat === 'number'
// Calculate exp-based deadline regardless of iat presence
if (!ignoreExpiration && typeof payload.exp === 'number') {
cacheValue[2] = payload.exp * 1000 + clockTolerance
}
const maxTTL = clockTimestamp + clockTolerance + cacheTTL
cacheValue[2] = cacheValue[2] === 0 ? maxTTL : Math.min(cacheValue[2], maxTTL)
Key Change: Move the exp check outside the if (hasIat) block so it executes regardless of whether iat is present.
Long-Term Remediation Strategy
Upgrade to Patched fast-jwt Version
Action: Monitor the fast-jwt GitHub repository for a patched release.
# Check current version
npm list fast-jwt
# Upgrade to latest
npm update fast-jwt
# Or install specific patched version (when available)
npm install fast-jwt@X.Y.Z
Verification:
const { createVerifier } = require('fast-jwt')
const pkg = require('fast-jwt/package.json')
console.log('fast-jwt version:', pkg.version)
// Verify patch is applied by running the reproduction test above
Update Token Issuer Configurations
Audit all token issuers in your organization:
- OAuth2 authorization servers
- Session token generators
- API key issuers
- Microservice-to-microservice token generators
Ensure all include iat claim:
// Example: Express.js middleware
app.post('/login', (req, res) => {
const now = Math.floor(Date.now() / 1000)
const token = jwt.sign(
{
sub: req.user.id,
iat: now, // Required
exp: now + 3600, // 1 hour
scope: 'api'
},
process.env.JWT_SECRET
)
res.json({ token })
})
Implement Monitoring for Cache-Related Authentication Anomalies
Set up alerts for:
- Tokens accepted after expiration:
if (token.exp < Date.now() / 1000) {
logger.warn('Expired token accepted', { userId: token.sub, exp: token.exp })
}
- Cache hit rate anomalies:
const cacheHitRate = cacheHits / (cacheHits + cacheMisses)
if (cacheHitRate > 0.95) {
logger.warn('Unusually high cache hit rate', { rate: cacheHitRate })
}
- Tokens without iat claim:
if (!token.iat) {
logger.warn('Token missing iat claim', { sub: token.sub })
}
Frequently Asked Questions
Is iat Required in a Standard JWT?
No. According to RFC 7519 (JSON Web Token Specification), the iat (Issued At) claim is optional. A JWT containing exp without iat is fully compliant with the JWT standard.
However, best practices recommend including iat for:
- Token age validation
- Replay attack prevention
- Audit logging
- Cache deadline calculation (as in fast-jwt)
Why Does the Caching Mechanism Bypass Expiration Validation?
Fast-jwt returns cached payloads early to avoid expensive cryptographic re-verification. This is a valid performance optimization. However, because exp validation happens inside verifyToken() (which is skipped on cache hits), the cache entry itself must maintain an accurate deadline based on exp.
When iat is missing, fast-jwt fails to set this deadline based on exp, defaulting instead to a generic 10-minute cache TTL. This causes the cache deadline to diverge from the token’s actual expiration time.
The Fix: Calculate the cache deadline based on exp regardless of whether iat is present.
What Is the Default Vulnerability Window After Token Expiration?
By default, cacheTTL is 10 minutes (600,000 milliseconds). This means an expired token without iat can remain accepted for up to 10 minutes after its official expiration time.
The actual window depends on:
- When the token was first cached (relative to its expiration)
- The configured
cacheTTLvalue - The
clockTolerancesetting
Example:
- Token expires at T=1000s
- Token cached at T=950s
- Cache deadline set to T=950s + 600s = T=1550s
- Vulnerability window: T=1000s to T=1550s (550 seconds)
How Do I Know If My Organization Uses fast-jwt with Caching Enabled?
Search your codebase:
# Find all fast-jwt imports
grep -r "from 'fast-jwt'" src/
grep -r "require('fast-jwt')" src/
# Find verifier configurations
grep -r "createVerifier" src/
# Check for cache: true
grep -r "cache: true" src/
Review package.json:
grep "fast-jwt" package.json
Audit configuration files:
.envfilesconfig/directory- Environment variable documentation
Can I Patch This Without Upgrading fast-jwt?
Yes. If you cannot upgrade immediately, you can manually patch src/verifier.js in your node_modules/fast-jwt/ directory:
// Find lines 127-146 and apply the patch shown in "Apply Source Code Patch" section
However, this is a temporary solution. Manual patches are overwritten when dependencies are reinstalled. Use this only as a bridge until an official patch is released.
Better approach: Use a patch tool like patch-package:
npm install --save-dev patch-package
# Make your changes to node_modules/fast-jwt/src/verifier.js
npx patch-package fast-jwt
# Commit patches/fast-jwt+X.Y.Z.patch to version control
References
Source Code:
fast-jwt/src/verifier.js(Lines 127–140:hasIatcheck andexpderivation)fast-jwt/src/verifier.js(Lines 142–146:maxTTLfallback logic)fast-jwt/src/verifier.js(Lines 372–388: Cache lookup bypassingverifyToken)
Standards & Specifications:
Related Security Advisories:
- OWASP: Broken Authentication
- CWE-613: Insufficient Session Expiration
Tools & Resources:
- jwt.io – JWT debugger and documentation
- CVSS Calculator – Severity scoring
Document Version: 1.0
Last Updated: 2024
Classification: Security Advisory
Distribution: IT Security Teams, Development Teams, DevOps