Symlink Attack with Jwt Tokens
How Symlink Attack Manifests in Jwt Tokens
Symlink attacks in JWT token systems exploit the way file paths are handled when validating or storing token-related data. The attack typically occurs when an application uses user-controlled input to construct file paths for token storage, verification, or revocation without proper validation.
A common Jwt Tokens-specific vulnerability arises when token revocation lists are stored in file-based systems. Consider a system that stores revoked JWT tokens in files named after the token ID:
def revoke_token(token_id):
file_path = f"/var/revoked_tokens/{token_id}.txt"
with open(file_path, 'w') as f:
f.write("REVOKED")An attacker can craft a token_id containing path traversal sequences like ../../../../etc/passwd, causing the application to overwrite critical system files instead of creating a token revocation record.
Another Jwt Tokens-specific manifestation occurs during token key management. Many systems store JWT signing keys in files where the key ID from the token header determines the file path:
def get_signing_key(jwt_header):
key_id = jwt_header.get('kid')
key_path = f"/keys/{key_id}.pem"
return open(key_path).read()If an attacker controls the kid header value and the system doesn't validate it, they can use symlink attacks to point to arbitrary files on the system, potentially exposing sensitive keys or causing denial of service.
Time-based token validation can also be exploited. Some systems store token creation timestamps in files for replay protection:
const tokenTimePath = `/var/token_times/${token.jti}`;
const tokenTime = fs.readFileSync(tokenTimePath, 'utf8');
if (Date.now() - tokenTime > MAX_AGE) {
throw new Error('Token expired');
}Symlinking the token time file to a critical system file could cause the application to read sensitive data or crash when attempting to parse it as a timestamp.
Jwt Tokens-Specific Detection
Detecting symlink attacks in JWT token systems requires examining both the token processing logic and the file system interactions. Start by auditing how your application handles JWT claims that could contain file paths or identifiers.
Key detection areas for Jwt Tokens systems:
- Token ID (jti) handling: Check if jti claims are used to construct file paths without validation
- Key ID (kid) processing: Verify that kid headers are validated against a whitelist of known keys
- Claim-based file operations: Audit any claims used to determine file paths for token storage or validation
- Token revocation mechanisms: Ensure revocation lists don't use user-controlled identifiers for file naming
Manual code review should focus on these Jwt Tokens-specific patterns:
# Vulnerable pattern - user input in file path
file_path = f"/tokens/{jwt['sub']}.jwt"
# Secure pattern - validate against whitelist
if jwt['sub'] not in VALID_USERS:
raise ValueError('Invalid user')
file_path = f"/tokens/{jwt['sub']}.jwt"For automated detection, middleBrick's Jwt Tokens scanning includes specific checks for symlink vulnerabilities in token processing systems. The scanner examines:
- Whether token claims are used to construct file paths without validation
- If JWT signing key retrieval uses user-controlled key IDs
- Token revocation mechanisms that might be susceptible to path traversal
- Configuration files that might contain JWT-related file paths
middleBrick's black-box scanning can detect if your Jwt Tokens endpoints are vulnerable to symlink-based attacks by testing for path traversal in token processing without requiring access to your source code. The scanner attempts to trigger symlink behavior through JWT claim manipulation and analyzes the system's response patterns.
Jwt Tokens-Specific Remediation
Remediating symlink attacks in Jwt Tokens systems requires a defense-in-depth approach that validates all user-controlled inputs and restricts file system access patterns. Here are Jwt Tokens-specific fixes:
1. Validate JWT claims before file operations:
import re
from jwt import decode
def validate_token_claims(jwt_token):
# Only allow alphanumeric token IDs
if not re.match(r'^[a-zA-Z0-9_-]{8,64}$', jwt_token['jti']):
raise ValueError('Invalid token ID format')
# Validate user claims against known users
if jwt_token['sub'] not in VALID_USERS:
raise ValueError('Unknown user')
# Check for malicious characters in any claim
for claim, value in jwt_token.items():
if isinstance(value, str) and re.search(r'[\\/]', value):
raise ValueError(f'Malicious characters in claim {claim}')
return True2. Use safe key ID handling:
from jwt import decode
VALID_KEY_IDS = {'key1', 'key2', 'key3'} # Known valid keys
def get_signing_key(jwt_header):
key_id = jwt_header.get('kid')
# Validate key ID against whitelist
if key_id not in VALID_KEY_IDS:
raise ValueError('Invalid key ID')
# Use a secure mapping instead of file paths
key_map = {
'key1': b'secret-key-1',
'key2': b'secret-key-2',
'key3': b'secret-key-3'
}
return key_map[key_id]3. Implement secure token storage:
const crypto = require('crypto');
const path = require('path');
function getSecureTokenPath(tokenId) {
// Hash the token ID to prevent path traversal
const hashedId = crypto.createHash('sha256').update(tokenId).digest('hex');
// Use a fixed directory with no user control
const basePath = path.join(__dirname, 'token_storage');
// Construct path safely
return path.join(basePath, `${hashedId}.jwt`);
}
// Verify the resolved path is within the allowed directory
function verifyPath(tokenId) {
const fullPath = getSecureTokenPath(tokenId);
if (!fullPath.startsWith(path.join(__dirname, 'token_storage'))) {
throw new Error('Path traversal detected');
}
return fullPath;
}4. Use database storage instead of file-based systems:
from sqlalchemy import Column, String, Boolean
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class RevokedToken(Base):
__tablename__ = 'revoked_tokens'
jti = Column(String(36), primary_key=True)
revoked_at = Column(DateTime, default=datetime.utcnow)
@classmethod
def is_revoked(cls, jti):
"""Check if token is revoked using database query"""
return session.query(cls).filter_by(jti=jti).first() is not None
# Usage in JWT middleware
def jwt_required(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
token = get_token_from_header()
decoded = decode(token, SECRET_KEY)
# Check revocation using database, not file system
if RevokedToken.is_revoked(decoded['jti']):
raise JWTError('Token revoked')
return fn(*args, **kwargs)
return wrapper5. Implement proper error handling:
const jwt = require('jsonwebtoken');
function secureVerify(token) {
try {
// Use a timeout to prevent resource exhaustion
return jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256', 'RS256'],
maxAge: '24h'
});
} catch (err) {
// Log without exposing sensitive details
console.warn('JWT verification failed:', err.name);
throw new Error('Invalid token');
}
}