HIGH excessive data exposurejwt tokens

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"
done

middleBrick'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 IDNameSeverity
CWE-915Mass Assignment HIGH

Frequently Asked Questions

How can I tell if my JWT tokens contain excessive data?
Decode your JWT tokens and examine the payload. Look for sensitive fields like passwords, API keys, personal information, or system identifiers. Tools like middleBrick automatically scan for excessive data exposure by analyzing token contents and flagging suspicious patterns. A good rule: if the token contains anything beyond authentication claims (sub, role, permissions) and expiration timestamps, it likely has excessive exposure.
What's the maximum safe size for a JWT token?
JWT tokens should be kept under 500-600 bytes when possible. Larger tokens increase bandwidth usage, create storage issues in databases and logs, and become more attractive targets for attackers. middleBrick's scanner flags tokens exceeding typical size thresholds and identifies which claims contribute most to token bloat. Remember that JWTs are sent with every API request, so even small size reductions compound across thousands of requests.