Excessive Data Exposure with Jwt Tokens
How Excessive Data Exposure Manifests in JWT Tokens
Excessive Data Exposure in JWT tokens occurs when applications embed more information than necessary in the token payload, creating attack surfaces beyond the intended authentication purpose. JWT tokens are base64url encoded JSON objects containing three parts: header, payload, and signature. The payload section often becomes a dumping ground for user data that should remain confidential.
A common manifestation is including complete user profiles in JWT tokens. Consider this vulnerable implementation:
const jwtToken = jwt.sign({
id: user.id,
email: user.email,
name: user.name,
role: user.role,
permissions: user.permissions,
address: user.address,
phone: user.phone,
lastLogin: user.lastLogin,
preferences: user.preferences,
paymentMethods: user.paymentMethods
}, process.env.JWT_SECRET, { expiresIn: '1h' });This token contains sensitive data like payment methods and addresses that should never travel with every API request. Attackers who intercept tokens via XSS, network sniffing, or server logs gain immediate access to this information.
Another pattern involves exposing internal system identifiers. Developers often include database IDs, internal flags, or system-specific metadata:
const token = jwt.sign({
userId: user.id, // Internal DB ID
isAdmin: user.isAdmin, // Internal flag
departmentId: user.dept.id, // Internal mapping
isActive: user.active, // Internal state
failedLogins: user.failedLogins // Security data
}, secret);Database IDs can enable enumeration attacks. If userId=1 belongs to Alice, an attacker might try userId=2 to access Bob's data. Internal flags like isAdmin become target variables for privilege escalation attempts.
Business logic exposure represents another risk. JWT tokens sometimes carry workflow state or application-specific data:
const workflowToken = jwt.sign({
orderId: order.id,
orderTotal: order.total,
items: order.items.map(i => ({ id: i.id, quantity: i.quantity })),
shippingAddress: order.shippingAddress,
paymentStatus: order.payment.status,
discountApplied: order.discount.code
}, secret);This exposes complete order details, pricing information, and discount codes to anyone who can read the token. An attacker could analyze pricing patterns, extract discount codes for reuse, or determine inventory levels from item quantities.
Third-party integration tokens often compound the problem by including service-specific data:
const integrationToken = jwt.sign({
userId: user.id,
provider: 'stripe',
providerUserId: stripeCustomerId,
providerTokens: {
accessToken: stripeAccessToken,
refreshToken: stripeRefreshToken
},
linkedAccounts: user.linkedAccounts.map(a => ({
provider: a.provider,
accountId: a.accountId,
connectedAt: a.connectedAt
}))
}, secret);Embedding provider tokens or account mappings creates significant exposure if the JWT is compromised. The token now contains credentials that could be used to impersonate the user across integrated services.
JWT-Specific Detection
Detecting excessive data exposure in JWT tokens requires both static analysis and runtime inspection. Start by examining your token generation code for unnecessary claims. Look for patterns where entire user objects or database entities are serialized into tokens.
Runtime detection involves decoding tokens to inspect their contents. Here's a simple Node.js script to analyze JWT payloads:
const jwt = require('jsonwebtoken');
function analyzeJwtPayload(token, secret) {
try {
const decoded = jwt.decode(token, { complete: true });
if (!decoded || !decoded.payload) {
console.log('Invalid JWT format');
return;
}
const payload = decoded.payload;
console.log('JWT Payload Analysis:');
console.log('Sensitive fields detected:', detectSensitiveFields(payload));
console.log('Data volume:', JSON.stringify(payload).length, 'bytes');
console.log('Claim categories:', categorizeClaims(payload));
} catch (error) {
console.log('Error decoding JWT:', error.message);
}
}
function detectSensitiveFields(payload) {
const sensitivePatterns = [
/password/i, /secret/i, /key/i, /token/i,
/credit.?card/i, /payment/i, /billing/i,
/address/i, /phone/i, /email/i,
/ssn/i, /dob/i, /government/i
];
return Object.keys(payload).filter(key =>
sensitivePatterns.some(pattern => pattern.test(key))
);
}
function categorizeClaims(payload) {
const categories = {
authentication: ['sub', 'aud', 'iss', 'exp', 'iat', 'nbf'],
authorization: ['role', 'permissions', 'scope'],
userProfile: ['name', 'email', 'phone'],
systemData: ['id', 'createdAt', 'updatedAt'],
businessData: ['orderId', 'amount', 'status']
};
const classification = { unknown: [] };
Object.keys(categories).forEach(cat => classification[cat] = []);
Object.keys(payload).forEach(key => {
let matched = false;
Object.keys(categories).forEach(cat => {
if (categories[cat].includes(key)) {
classification[cat].push(key);
matched = true;
}
});
if (!matched) classification.unknown.push(key);
});
return classification;
}
// Usage
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
analyzeJwtPayload(token, process.env.JWT_SECRET);Automated scanning tools like middleBrick can detect excessive data exposure by analyzing JWT tokens in transit. The scanner identifies tokens with suspicious claim patterns, oversized payloads, and sensitive field names. middleBrick's black-box scanning tests unauthenticated endpoints to find tokens that expose data without requiring valid credentials.
Network traffic analysis provides another detection layer. JWT tokens appear in Authorization headers (Bearer <token>), cookies, or URL parameters. Capturing and decoding these tokens reveals what data travels with each request:
# Capture JWT tokens from network traffic
tcpdump -i eth0 -s 0 -w jwt_traffic.pcap port 443
# Extract JWTs from PCAP
tshark -r jwt_traffic.pcap -Y "http.request.method == POST || http.request.method == GET" -T fields -e http.header.authorization | grep -o '[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'
# Decode tokens
for token in $(cat jwt_tokens.txt); do
echo "=== $token ==="
echo $token | cut -d'.' -f1 | base64 -d 2>/dev/null | jq . 2>/dev/null || echo "Invalid header"
echo $token | cut -d'.' -f2 | base64 -d 2>/dev/null | jq . 2>/dev/null || echo "Invalid payload"
donemiddleBrick's API security scanning specifically tests for JWT token exposure patterns, including tokens that contain PII, API keys, or system identifiers. The scanner provides severity ratings and remediation guidance for each finding.
JWT-Specific Remediation
Remediating excessive data exposure in JWT tokens requires architectural changes to minimize token payload. The core principle: JWT tokens should carry only what's necessary for authentication and authorization.
Start with minimal authentication tokens:
const authOnlyToken = jwt.sign({
sub: user.id, // Subject: user identifier
role: user.role, // Authorization role
permissions: ['read', 'write'], // Minimal permissions
exp: Math.floor(Date.now() / 1000) + 3600
}, process.env.JWT_SECRET, { expiresIn: '1h' });This token contains only what's needed to verify identity and check permissions. No personal data, no business logic, no system identifiers beyond the user ID.
For scenarios requiring additional data, use tokenless approaches. Instead of embedding data in JWTs, pass identifiers and fetch data server-side:
// BAD: Data in token
const orderToken = jwt.sign({
orderId: 12345,
orderTotal: 99.99,
items: [{ id: 1, name: 'Product A' }]
}, secret);
// GOOD: Reference only, fetch data
const referenceToken = jwt.sign({
orderId: 12345,
exp: Math.floor(Date.now() / 1000) + 300
}, secret);
// Server validates token, then fetches order details
app.get('/api/orders/:id', authenticateToken, (req, res) => {
const orderId = req.params.id;
const order = await getOrderFromDatabase(orderId);
res.json(order);
});Implement token segmentation for complex workflows. Use short-lived access tokens for authentication and separate tokens for specific operations:
// Access token: short-lived, minimal claims
const accessToken = jwt.sign({
sub: user.id,
role: user.role,
exp: Math.floor(Date.now() / 1000) + 900
}, process.env.JWT_SECRET, { expiresIn: '15m' });
// Permission token: issued on-demand for specific operations
async function createPermissionToken(userId, resourceId, action) {
const resource = await getResource(resourceId);
if (!resource || !canUserAccessResource(userId, resourceId, action)) {
throw new Error('Unauthorized');
}
return jwt.sign({
sub: userId,
resource: resourceId,
action: action,
exp: Math.floor(Date.now() / 1000) + 300
}, process.env.JWT_SECRET, { expiresIn: '5min' });
}Use claims whitelisting to prevent accidental inclusion of sensitive data:
const allowedClaims = ['sub', 'role', 'permissions', 'exp', 'iat', 'nbf'];
function createSecureToken(user, claims) {
const filteredClaims = {};
allowedClaims.forEach(claim => {
if (claims.hasOwnProperty(claim)) {
filteredClaims[claim] = claims[claim];
}
});
// Add only verified user data
filteredClaims.sub = user.id;
filteredClaims.role = user.role;
return jwt.sign(filteredClaims, process.env.JWT_SECRET);
}
// Usage
const token = createSecureToken(user, {
permissions: user.permissions,
exp: Math.floor(Date.now() / 1000) + 3600
});Implement token size monitoring to detect when tokens grow unexpectedly:
const MAX_TOKEN_SIZE = 500; // bytes
function validateTokenSize(token) {
const decoded = jwt.decode(token);
if (!decoded) return false;
const payloadSize = JSON.stringify(decoded).length;
if (payloadSize > MAX_TOKEN_SIZE) {
console.warn(`Token size excessive: ${payloadSize} bytes`);
return false;
}
return true;
}
// Middleware to check token size
app.use((req, res, next) => {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7);
if (!validateTokenSize(token)) {
return res.status(400).json({ error: 'Token size excessive' });
}
}
next();
});middleBrick's continuous monitoring can alert when token sizes exceed thresholds or when new sensitive claims appear in tokens. The Pro plan's scheduled scanning catches these issues in staging environments before production deployment.
Related CWEs: propertyAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-915 | Mass Assignment | HIGH |