HIGH bola idorjwt tokens

Bola Idor with Jwt Tokens

How BOLA/IDOR Manifests in JWT Tokens

Broken Object Level Authorization (BOLA), also known as Insecure Direct Object Reference (IDOR), occurs when an application fails to properly verify whether a user has permission to access specific objects. JWT tokens, while excellent for stateless authentication, can introduce unique BOLA vulnerabilities when object identifiers are embedded in token claims without proper authorization checks.

The most common JWT BOLA pattern involves user IDs or object IDs stored in token claims. Consider this flawed implementation:

// Vulnerable: trusting token claims without verification
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.userId; // Trust this without validation
const postId = req.params.postId;

// NO authorization check!
const post = await getPostById(postId);
res.json(post);

The vulnerability here is that any authenticated user can modify their JWT token's userId claim to access any post. Since JWT tokens are cryptographically signed, users cannot forge tokens, but they can easily change the userId claim if the application doesn't verify it against the authenticated identity.

Another JWT-specific BOLA pattern involves role-based access embedded in tokens:

// Vulnerable: role escalation via token modification
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const role = decoded.role; // Admin? User? Trust without validation

// NO role verification!
const allUsers = await getAllUsers();
res.json(allUsers);

Attackers can modify their token's role claim from user to admin, gaining unauthorized access to administrative endpoints. The cryptographic signature prevents token forgery but doesn't prevent legitimate users from modifying claims if the application doesn't validate them.

Database ID exposure through JWT claims creates another attack vector:

// Vulnerable: exposing sequential IDs in tokens
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const accountId = decoded.accountId; // Sequential ID exposure

// NO authorization check!
const account = await getAccountById(accountId);
res.json(account);

Even with proper token signing, sequential IDs allow attackers to enumerate and access other users' accounts by simply incrementing the ID in their token.

JWT-Specific Detection Methods

Detecting JWT-specific BOLA vulnerabilities requires examining both the token structure and the authorization logic. Here's how to identify these issues in your JWT implementation:

1. Token Claim Analysis

Examine which claims your application trusts without validation. Common problematic claims include:

// Scan for vulnerable patterns
const fs = require('fs');
const code = fs.readFileSync('server.js', 'utf8');

// Detect claims used without validation
const vulnerablePatterns = [
  /jwt\.verify\(.*\).*[\n\r].*const (userId|role|accountId|ownerId) = decoded\.[a-zA-Z]+;/g,
  /decoded\.[a-zA-Z]+.*=.*req\.(params|query|body)/g
];

vulnerablePatterns.forEach(pattern => {
  const matches = code.match(pattern);
  if (matches) {
    console.log('Potential BOLA vulnerability:', matches);
  }
});

2. Authorization Logic Verification

Check if your endpoints verify that the authenticated user matches the resource owner:

// Test for missing authorization checks
function hasAuthorizationCheck(code) {
  const patterns = [
    /const.*=.*decoded\..*;/,
    /const.*=.*req\.(params|query|body)/,
    /await.*get.*ById.*\(.*\)/
  ];
  
  // Look for missing ownership verification
  const missingCheck = code.match(
    new RegExp(
      `${patterns[0].source}.*${patterns[2].source}.*res\.json`, 
      's'
    )
  );
  
  return missingCheck;
}

3. Automated Scanning with middleBrick

middleBrick's black-box scanning approach can detect JWT BOLA vulnerabilities without requiring source code access:

# Scan JWT-protected endpoints
middlebrick scan https://api.example.com/user/123/posts \
  --auth "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# middleBrick tests for:
# - Parameter manipulation (changing user IDs in JWT claims)
# - Role escalation attempts
# - Sequential ID enumeration
# - Missing authorization checks

The scanner automatically tests for BOLA by manipulating JWT claims and verifying whether the application properly enforces access controls. It checks if changing user IDs in tokens grants access to unauthorized resources.

4. Runtime Monitoring

Implement monitoring to detect BOLA attempts:

const winston = require('winston');

function monitorAuthorization(req, res, next) {
  const start = Date.now();
  const originalSend = res.send;
  
  res.send = function(data) {
    const duration = Date.now() - start;
    
    // Log suspicious patterns
    if (duration < 10 && req.method === 'GET') {
      winston.warn({
        message: 'Potential BOLA attempt',
        endpoint: req.path,
        userId: req.user?.id,
        resourceId: req.params.id || req.body.id,
        ip: req.ip
      });
    }
    
    originalSend.call(this, data);
  };
  
  next();
}

JWT-Specific Remediation Techniques

Fixing JWT BOLA vulnerabilities requires implementing proper authorization checks and avoiding trust in token claims. Here are JWT-specific remediation strategies:

1. Never Trust Token Claims for Authorization

Always validate token claims against the actual authenticated user and resource ownership:

// Secure implementation
const jwt = require('jsonwebtoken');

async function secureEndpoint(req, res) {
  try {
    const token = req.headers.authorization.split(' ')[1];
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Verify token user matches the requested resource
    const requestedUserId = req.params.userId || decoded.userId;
    if (decoded.userId !== requestedUserId) {
      return res.status(403).json({ 
        error: 'Access denied - resource ownership mismatch' 
      });
    }
    
    // Fetch resource with proper ownership verification
    const resource = await getResourceByOwner(decoded.userId, req.params.resourceId);
    if (!resource) {
      return res.status(404).json({ error: 'Resource not found' });
    }
    
    res.json(resource);
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

2. Implement Database-Level Authorization

Use database queries that enforce ownership at the data layer:

// Secure database query with ownership verification
async function getPostByIdAndOwner(postId, ownerId) {
  const query = `
    SELECT * FROM posts 
    WHERE id = $1 AND owner_id = $2
    AND deleted_at IS NULL
  `;
  
  const result = await db.query(query, [postId, ownerId]);
  return result.rows[0];
}

// Usage
app.get('/posts/:postId', async (req, res) => {
  const token = req.headers.authorization.split(' ')[1];
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  
  const post = await getPostByIdAndOwner(req.params.postId, decoded.userId);
  
  if (!post) {
    return res.status(404).json({ error: 'Post not found or access denied' });
  }
  
  res.json(post);
});

3. Use Non-Sequential Identifiers

Replace sequential IDs with UUIDs or other non-guessable identifiers:

// Generate UUIDs instead of sequential IDs
const { v4: uuidv4 } = require('uuid');

// Database schema
CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  owner_id UUID REFERENCES users(id),
  content TEXT,
  created_at TIMESTAMP DEFAULT NOW()
);

// Secure JWT handling
app.get('/posts/:postId', async (req, res) => {
  const token = req.headers.authorization.split(' ')[1];
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  
  // Parse UUID and validate format
  const postId = req.params.postId;
  if (!isValidUUID(postId)) {
    return res.status(400).json({ error: 'Invalid post ID format' });
  }
  
  const post = await getPostByIdAndOwner(postId, decoded.userId);
  
  if (!post) {
    return res.status(404).json({ error: 'Post not found or access denied' });
  }
  
  res.json(post);
});

4. Implement Role-Based Access Control (RBAC) with Database Verification

Never trust role claims in JWT tokens. Verify roles against the database:

// Secure RBAC implementation
async function verifyRole(userId, requiredRole) {
  const result = await db.query(
    'SELECT role FROM users WHERE id = $1',
    [userId]
  );
  
  if (!result.rows[0]) {
    return false;
  }
  
  const userRole = result.rows[0].role;
  const roleHierarchy = { user: 1, admin: 2, superadmin: 3 };
  
  return roleHierarchy[userRole] >= roleHierarchy[requiredRole];
}

app.get('/admin/users', async (req, res) => {
  const token = req.headers.authorization.split(' ')[1];
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  
  const hasAccess = await verifyRole(decoded.userId, 'admin');
  if (!hasAccess) {
    return res.status(403).json({ error: 'Admin access required' });
  }
  
  const users = await db.query('SELECT id, email, role FROM users');
  res.json(users.rows);
});

5. Use Short-Lived Tokens with Refresh Mechanism

Implement short-lived JWT tokens with refresh tokens to reduce the window of opportunity for BOLA attacks:

// Secure token generation
const generateTokens = (userId) => {
  const accessToken = jwt.sign(
    { userId, type: 'access' },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );
  
  const refreshToken = jwt.sign(
    { userId, type: 'refresh' },
    process.env.REFRESH_SECRET,
    { expiresIn: '7d' }
  );
  
  return { accessToken, refreshToken };
};

// Secure refresh endpoint
app.post('/refresh', async (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.REFRESH_SECRET);
    
    // Verify refresh token belongs to user
    const user = await getUserById(decoded.userId);
    if (!user || user.refresh_token !== token) {
      return res.status(401).json({ error: 'Invalid refresh token' });
    }
    
    const newTokens = generateTokens(decoded.userId);
    res.json(newTokens);
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
});

Related CWEs: bolaAuthorization

CWE IDNameSeverity
CWE-250Execution with Unnecessary Privileges HIGH
CWE-639Insecure Direct Object Reference CRITICAL
CWE-732Incorrect Permission Assignment HIGH

Frequently Asked Questions

How can I test if my JWT implementation has BOLA vulnerabilities?
Use middleBrick's automated scanning to test your JWT endpoints. The scanner manipulates token claims and tests for unauthorized access. You can also manually test by modifying the userId or role claims in your JWT token and attempting to access resources that don't belong to you. Look for endpoints that return data without proper ownership verification.
What's the difference between JWT BOLA and regular BOLA?
JWT BOLA specifically involves vulnerabilities in JWT token implementations where claims like userId, role, or accountId are trusted without validation. Regular BOLA can occur with any authentication method. JWT BOLA is unique because the token's cryptographic signature creates a false sense of security—users can't forge tokens but can modify claims if the application doesn't validate them against the authenticated identity.