Broken Function Level Authorization

Broken Function Level Authorization in Node.js (Express) [CVE-2026-4171]

[Updated Mar 2026] Updated CVE-2026-4171

Overview

This guide discusses CVE-2026-4171 affecting CodeGenieApp serverless-express up to version 4.17.1, where manipulating the userId parameter in the TodoList.ts component enables remote authorization bypass (CWE-285, CWE-639). The vulnerability illustrates broken function-level authorization: endpoints rely on user-supplied identifiers rather than enforcing ownership, allowing an attacker to access or manipulate another user's resources by altering the userId in the request. The impact in real Node.js (Express) apps is data exposure and potential data modification across user boundaries when authorization checks are not anchored to the authenticated identity. The issue is critical for any API that scopes data by userId and uses function-level access controls rather than resource-based checks. The CVE highlights how such weaknesses can be exposed in serverless Express patterns and how they align with CWE-285 and CWE-639 by bypassing intended access controls through parameter manipulation.

Affected Versions

CodeGenieApp serverless-express <= 4.17.1

Code Fix Example

Node.js (Express) API Security Remediation
Vulnerable:
```js
// Vulnerable pattern: relies on user-supplied userId for authorization
const express = require('express');
const app = express();

// Mock data access
const TodoList = {
  findOne: async ({ where }) => {
    // pretend DB access returns a todo list if owner matches
    const data = {
      id: 'todo-123',
      userId: 'user-42',
      title: 'Buy milk'
    };
    return where.id === data.id && where.userId === data.userId ? data : null;
  }
};

// Authentication middleware populates req.user
app.use((req, res, next) => {
  req.user = { id: 'user-42' }; // normally from JWT/OAuth
  next();
});

// Vulnerable endpoint: userId from path is used to scope data without verifying ownership
app.get('/api/users/:userId/lists/:listId', async (req, res) => {
  const { userId, listId } = req.params;
  const list = await TodoList.findOne({ where: { id: listId, userId } });
  if (!list) return res.status(404).send('Not found');
  res.json(list);
});

app.listen(3000, () => console.log('Server running'));
```

Fixed:
```js
// Fixed pattern: enforce ownership using authenticated identity and/or ignore the userId param for authorization decisions
const express = require('express');
const app = express();

// Mock data access
const TodoList = {
  findOne: async ({ where }) => {
    const data = {
      id: 'todo-123',
      userId: 'user-42',
      title: 'Buy milk'
    };
    return where.id === data.id && where.userId === data.userId ? data : null;
  }
};

// Authentication middleware populates req.user
app.use((req, res, next) => {
  req.user = { id: 'user-42' }; // normally from JWT/OAuth
  next();
});

// Secure endpoint: verify that the requester owns the resource; prefer using req.user.id rather than trusting req.params.userId
app.get('/api/users/:userId/lists/:listId', async (req, res) => {
  const { listId } = req.params;
  const ownerId = req.user && req.user.id;
  if (!ownerId) return res.status(401).send('Unauthorized');
  // Option 1: enforce ownership strictly (use token subject for queries)
  const list = await TodoList.findOne({ where: { id: listId, userId: ownerId } });
  // Option 2 (alternative): verify the path param userId matches the token owner, then query
  // if (req.params.userId !== ownerId) return res.status(403).json({ error: 'Forbidden' });
  if (!list) return res.status(404).send('Not found');
  res.json(list);
});

app.listen(3000, () => console.log('Server running'));
```

CVE References

Choose which optional cookies to allow. You can change this any time.