Session Fixation with Jwt Tokens
How Session Fixation Manifests in Jwt Tokens
Session fixation attacks in JWT tokens exploit the predictable nature of token generation and handling. Unlike traditional session IDs that are stored server-side, JWTs are self-contained and stateless, creating unique vulnerability patterns.
The most common JWT session fixation scenario occurs when an attacker obtains a valid JWT token (through phishing, interception, or other means) and forces a victim to use that specific token. Since JWTs are often stored in HTTP-only cookies or local storage, the attack can be subtle and difficult to detect.
Consider this vulnerable pattern in Node.js/Express:
app.post('/login', (req, res) => {
const user = authenticate(req.body.email, req.body.password);
if (user) {
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.cookie('jwt', token, { httpOnly: true, secure: true });
res.json({ success: true });
}
});The vulnerability here is that the token generation is predictable and the server doesn't invalidate existing sessions. An attacker who obtains a token can:
- Steal a valid JWT token
- Force the victim to use that token (through session fixation techniques)
- Maintain access even after the victim changes their password
Another JWT-specific fixation pattern involves token replay attacks. Since JWTs are cryptographically signed but not encrypted by default, an attacker who captures a token can potentially reuse it:
// Vulnerable: No token invalidation on logout
app.post('/logout', (req, res) => {
res.clearCookie('jwt');
res.json({ success: true });
});This code only removes the cookie client-side but doesn't invalidate the token server-side. The token remains valid until expiration, allowing an attacker to replay it.
JWT session fixation also manifests in refresh token patterns. Many implementations fail to rotate refresh tokens, creating a window where compromised tokens remain valid:
// Vulnerable refresh pattern
app.post('/refresh', (req, res) => {
const token = req.cookies['refresh_token'];
if (token) {
const decoded = jwt.verify(token, process.env.REFRESH_SECRET);
const newAccessToken = jwt.sign(
{ userId: decoded.userId },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
}
});The vulnerability: if an attacker obtains the refresh token, they can generate new access tokens indefinitely without detection.
Jwt Tokens-Specific Detection
Detecting JWT session fixation requires examining both token generation patterns and token lifecycle management. Here are Jwt Tokens-specific detection methods:
Token Generation Analysis
Examine how tokens are generated for predictability. Look for:
- Static claims that don't include session-specific data
- Predictable token IDs (jti claims)
- Lack of cryptographic nonce in token generation
Token Lifecycle Monitoring
Track token usage patterns to identify fixation:
// Detection: Monitor for token reuse across different users
const tokenUsage = new Map();
app.use((req, res, next) => {
const token = req.cookies.jwt || req.headers.authorization;
if (token) {
const currentUserId = getUserIdFromToken(token);
if (tokenUsage.has(token)) {
const { userId, timestamp } = tokenUsage.get(token);
if (userId !== currentUserId) {
console.warn(`Token fixation detected: token reused by different user ${currentUserId}`);
}
} else {
tokenUsage.set(token, { userId: currentUserId, timestamp: Date.now() });
}
}
next();
});middleBrick Detection Capabilities
middleBrick scans for JWT-specific session fixation vulnerabilities through:
| Check Type | What It Tests | Detection Method |
|---|---|---|
| Token Predictability | Analyzes JWT structure for predictable claims | Pattern matching on token generation endpoints |
| Session Fixation | Tests if tokens can be reused across sessions | Active probing with multiple token variants |
| Refresh Token Security | Checks if refresh tokens are properly rotated | Analyzes refresh token endpoints and rotation logic |
middleBrick's black-box scanning approach tests the unauthenticated attack surface, attempting to:
- Obtain JWT tokens through various means
- Test token reuse across different user contexts
- Analyze token expiration and rotation patterns
- Check for secure token storage and transmission
Runtime Monitoring
Implement detection at the application level:
// Detection: Track active sessions per user
const activeSessions = new Map();
app.post('/login', (req, res, next) => {
const user = authenticate(req.body);
if (user) {
const token = generateJwt(user);
const sessionId = crypto.randomBytes(16).toString('hex');
// Track active sessions
if (!activeSessions.has(user.id)) {
activeSessions.set(user.id, new Set());
}
activeSessions.get(user.id).add(sessionId);
res.cookie('jwt', token, { httpOnly: true });
res.json({ success: true });
}
});This allows detection of unusual session patterns that might indicate fixation attacks.
Jwt Tokens-Specific Remediation
Remediating JWT session fixation requires implementing proper token lifecycle management and secure generation practices. Here are Jwt Tokens-specific solutions:
Token Rotation and Invalidation
Implement refresh token rotation to prevent fixation:
// Secure refresh pattern with rotation
app.post('/refresh', (req, res) => {
const token = req.cookies['refresh_token'];
if (!token) return res.status(401).json({ error: 'No token provided' });
try {
const decoded = jwt.verify(token, process.env.REFRESH_SECRET);
// Generate new refresh token
const newRefreshToken = jwt.sign(
{ userId: decoded.userId, version: decoded.version + 1 },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
);
const newAccessToken = jwt.sign(
{ userId: decoded.userId },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
// Send new refresh token in httpOnly cookie
res.cookie('refresh_token', newRefreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict'
});
res.json({ accessToken: newAccessToken });
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
});Secure Token Generation
Generate tokens with unpredictable claims and proper rotation:
function generateSecureJwt(user, sessionData = {}) {
const tokenId = crypto.randomBytes(16).toString('hex');
const issuedAt = Date.now();
return jwt.sign(
{
sub: user.id,
email: user.email,
jti: tokenId, // Unique token ID
iat: Math.floor(issuedAt / 1000),
exp: Math.floor((issuedAt + 15 * 60000) / 1000), // 15 minutes
...sessionData,
// Include unpredictable session data
session_nonce: crypto.randomBytes(8).toString('hex')
},
process.env.JWT_SECRET,
{ algorithm: 'HS256' }
);
}Session Invalidation on Logout
Properly invalidate tokens on logout:
// Blacklist approach for token invalidation
const tokenBlacklist = new Set();
const blacklistTimeout = 30 * 60 * 1000; // 30 minutes
app.post('/logout', (req, res) => {
const token = req.cookies.jwt || req.headers.authorization?.replace('Bearer ', '');
if (token) {
tokenBlacklist.add(token);
// Auto-cleanup
setTimeout(() => {
tokenBlacklist.delete(token);
}, blacklistTimeout);
}
res.clearCookie('jwt');
res.json({ success: true });
});
// Middleware to check blacklist
app.use((req, res, next) => {
const token = req.cookies.jwt || req.headers.authorization?.replace('Bearer ', '');
if (token && tokenBlacklist.has(token)) {
return res.status(401).json({ error: 'Token revoked' });
}
next();
});middleBrick Integration for Continuous Security
Integrate middleBrick into your security workflow to continuously test for fixation vulnerabilities:
# GitHub Action for JWT security testing
name: API Security Scan
on:
pull_request:
paths: ['src/api/**']
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run middleBrick scan
run: |
npx middlebrick scan https://api.yourdomain.com/login \
--output json \
--fail-below B \
--token ${{ secrets.MIDDLEBRICK_TOKEN }}
- name: Upload results
uses: actions/upload-artifact@v3
with:
name: security-report
path: middlebrick-report.json
This setup ensures that any JWT session fixation vulnerabilities are caught before deployment.
Additional Security Measures
Implement these Jwt Tokens-specific security patterns:
// Token binding to IP/user agent
function generateBoundJwt(user, req) {
const token = generateSecureJwt(user);
const bindingData = {
ip: req.ip,
userAgent: req.headers['user-agent'],
issuedAt: Date.now()
};
// Store binding data server-side
redisClient.set(`token:${token}`, JSON.stringify(bindingData), 'EX', 15 * 60);
return token;
}
// Verify token binding
app.use((req, res, next) => {
const token = req.cookies.jwt || req.headers.authorization?.replace('Bearer ', '');
if (token) {
redisClient.get(`token:${token}`, (err, bindingData) => {
if (bindingData) {
const { ip, userAgent } = JSON.parse(bindingData);
if (ip !== req.ip || userAgent !== req.headers['user-agent']) {
return res.status(401).json({ error: 'Token binding mismatch' });
}
}
next();
});
} else {
next();
}
});These Jwt Tokens-specific patterns significantly reduce the risk of session fixation attacks by adding unpredictability and server-side validation to the token lifecycle.