Back to Blog
critical SEVERITY5 min read

How Broken Object-Level Authorization happens in Express.js and how to fix it

A critical authorization flaw in `src/v1/routes/index.js` allowed any authenticated API key holder to access arbitrary budgets by manipulating the `budgetSyncId` URL parameter. The fix introduces an environment-based allowlist that validates budget access before processing requests.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 7, 2026•Reviewed September 7, 2026

Answer Summary

This vulnerability is Broken Object-Level Authorization (BOLA), also known as Insecure Direct Object Reference (IDOR), in an Express.js application (CWE-639). The `authorizeRequest` middleware at line 5 of `src/v1/routes/index.js` only validated API key authenticity, not authorization to access specific `budgetSyncId` values. Attackers could access any budget by changing the URL parameter. The fix adds an `allowedBudgetSyncIds` configuration and explicit authorization check before the `Budget()` constructor call, returning HTTP 403 for unauthorized budget IDs.

Vulnerability at a Glance

cweCWE-639 (Authorization Bypass by User-Controlled Key)
fixAdded environment-based allowlist with explicit budgetSyncId validation
riskAny authenticated user could read, modify, or delete any budget in the system
languageJavaScript/Node.js (Express.js)
root causeMissing authorization check between authentication and data access
vulnerabilityBroken Object-Level Authorization (BOLA/IDOR)

Title: How Broken Object-Level Authorization happens in Express.js and how to fix it

In the src/v1/routes/index.js file of a budget management API, we discovered a critical authorization bypass that allowed any authenticated user to access arbitrary financial data. The vulnerability—classified as Broken Object-Level Authorization (BOLA)—stems from a classic security anti-pattern: authenticating users without authorizing their specific resource access.

This flaw in the route handler at lines 5-14 meant that simply possessing a valid API key granted unrestricted access to every budget in the system. Let's examine how this happened and how the automated fix closed this dangerous gap.


The Vulnerability Explained

The Missing Authorization Check

The vulnerable code in src/v1/routes/index.js looked like this:

router.use('/budgets/:budgetSyncId', authorizeRequest, async (req, res, next) => {
    try {
      res.locals.budget = await Budget(req.params.budgetSyncId, req.get('budget-encryption-password'));
      next();
    } catch(err) {
      // error handling...
    }
});

The critical flaw: The authorizeRequest middleware only validates that the API key is legitimate. It does not check whether this specific API key is allowed to access the budgetSyncId provided in the URL path parameter.

Notice how req.params.budgetSyncId flows directly into the Budget() constructor at line 8 without any authorization verification. This creates a direct path from user input to data access.

How Attackers Exploited This

An attacker with any valid API key could:

  1. Enumerate budgets: Try sequential or predictable budgetSyncId values (e.g., budget-001, budget-002, personal-2024)
  2. Access sensitive financial data: View income, expenses, account balances, and transaction history for any budget in the system
  3. Modify arbitrary budgets: If the route supported POST/PUT/DELETE operations, mutate others' financial records

Example attack scenario:

GET /v1/budgets/company-payroll-2024
Headers: X-API-Key: {any_valid_key}

Even with a personal API key, the attacker receives the corporate payroll budget data because no check ties the API key to permitted budget IDs.

Real-World Impact

For this budget management application, the impact is severe:

  • Financial data exposure: Personal and business finances exposed to unauthorized parties
  • Regulatory violations: Potential GDPR, SOC 2, or PCI-DSS compliance failures
  • Data integrity risks: Unauthorized modifications to budget allocations
  • Reputational damage: Loss of user trust in financial data security

The Fix

The automated fix introduces a defense-in-depth authorization layer using an environment-based allowlist. Two files were modified:

1. Configuration Addition (src/config/config.js)

exports.config = {
  // ... existing config ...
  allowedBudgetSyncIds: process.env.ALLOWED_BUDGET_SYNC_IDS 
    ? process.env.ALLOWED_BUDGET_SYNC_IDS
        .split(',')
        .map(id => id.trim())
        .filter(Boolean) 
    : null,
  // ...
};

This adds a new allowedBudgetSyncIds array, populated from a comma-separated environment variable. The filter(Boolean) removes empty strings from malformed input.

2. Route Authorization Enforcement (src/v1/routes/index.js)

const { config } = require('../../config/config');

router.use('/budgets/:budgetSyncId', authorizeRequest, async (req, res, next) => {
    try {
      if (config.allowedBudgetSyncIds && 
          !config.allowedBudgetSyncIds.includes(req.params.budgetSyncId)) {
        res.status(403).json({"error": "Forbidden"});
        return;
      }
      res.locals.budget = await Budget(req.params.budgetSyncId, req.get('budget-encryption-password'));
      next();
    } catch(err) {
      // error handling...
    }
});

Key security improvements:

Aspect Before After
Authorization None Explicit allowlist check
Error response 500 (unhandled) or data leak Controlled 403 Forbidden
Configuration Hardcoded implicit trust Environment-driven explicit policy
Attack surface Any budgetSyncId Only enumerated IDs

The fix strategically places the authorization check after authentication (authorizeRequest) but before data access (Budget()). This follows the OWASP Authorization Cheat Sheet pattern of "early deny."

The config.allowedBudgetSyncIds && guard ensures backward compatibility—if the environment variable isn't set, the check is skipped (though production deployments should always configure this).


Key Takeaways

  • Authentication ≠ Authorization: The authorizeRequest middleware name was misleading—it only handled authentication. Always verify both explicitly.

  • URL parameters are untrusted: req.params.budgetSyncId at line 8 of src/v1/routes/index.js flowed directly to data access without validation—treat all route parameters as attacker-controlled.

  • Environment-based policies enable least privilege: The ALLOWED_BUDGET_SYNC_IDS configuration allows operators to enforce strict budget isolation without code changes.

  • Early authorization prevents information leakage: The fix returns 403 before any database query, preventing timing attacks that might reveal valid budgetSyncId values.

  • Audit your middleware chain: Review all Express middleware that runs before data access—gaps in this chain create authorization bypass opportunities.


How Orbis AppSec Detected This

Source: HTTP request parameter budgetSyncId in URL path (/budgets/:budgetSyncId)

Sink: Budget() constructor call at src/v1/routes/index.js:8 that loads budget data

Missing control: No authorization check between authorizeRequest middleware (authentication only) and the Budget() data access that would verify the API key holder is permitted to access the specific budgetSyncId

CWE: CWE-639: Authorization Bypass by User-Controlled Key

Fix: Added explicit allowlist validation using config.allowedBudgetSyncIds.includes(req.params.budgetSyncId) before the vulnerable data access, returning HTTP 403 for unauthorized requests.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

The BOLA vulnerability in src/v1/routes/index.js demonstrates how easily authorization gaps emerge when developers conflate "authenticated" with "authorized." The fix's elegant simplicity—adding a configuration-driven allowlist check—shows that robust security often requires just adding the right validation at the right point in your request pipeline.

For teams building Express.js APIs, this serves as a reminder: always ask "is this user allowed to access this specific resource?" not just "is this user legitimate?" The distinction protects your users' most sensitive data.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #112

Related Articles

critical

Discord Bot Auto-Role CWE-269: Privilege Escalation via role.position

A Discord.js bot's auto-role feature allowed privilege escalation where any user with ManageRoles permission could add Administrator roles to automatic assignment lists, regardless of their own role hierarchy. The vulnerability existed because role.position validation checked only the bot's permissions, not the invoking user's role hierarchy relative to the target role.

high

CVE-2026-54290: hono cors() Reflects Any Origin With Credentials

The `hono` CORS middleware, as resolved in this service at 4.12.8, reflected the caller's `Origin` header back in `Access-Control-Allow-Origin` while also emitting `Access-Control-Allow-Credentials: true` whenever the `origin` option was left at its `'*'` default. That combination makes any website a trusted origin for credentialed cross-origin reads. The dependency range was raised from `^4.7.1` to `^4.13.5`, moving the installed copy from 4.12.8 to 4.13.5.

critical

POST /api/generate Lacks Authentication, Allowing Unauthenticated

A resume generation endpoint in a Node.js backend accepted requests from any caller with network access, allowing attackers to consume OpenAI API quota without restriction. The vulnerability stemmed from missing authentication middleware on a cost-bearing endpoint. The fix adds mandatory API key validation via HTTP headers before processing any generation requests.

critical

SchedulePush.disableReminder Missing Authorization Check in push.js

The `disableReminder` method in the `SchedulePush` class allowed any user to disable push notification reminders for arbitrary user IDs by manipulating the `e.user_id` event parameter. The fix adds a `checkFriend()` authorization gate that verifies the requesting user has a valid friendship relationship with the bot before modifying subscription state.

high

How Missing Rate Limiting Enables Denial of Service Attacks in Node.js and How to Fix It

The k-skill-proxy server exposed multiple public API endpoints (`/health`, `/v1/vworld/search`, `/v1/fine-dust/report`, `/v1/assembly/bills`) without consistent rate limiting middleware, leaving them vulnerable to denial-of-service attacks. A `buildRateLimiter` function existed but wasn't applied to all endpoints. This fix ensures rate limiting is enforced on all public endpoints, preventing resource exhaustion attacks.

critical

proxy-addr 2.0.7 IP Spoofing: CVE-2026-90711 Trust Bypass

A critical vulnerability in proxy-addr 2.0.7 allowed attackers to spoof client IP addresses by manipulating X-Forwarded-For headers when the trust chain evaluation contained specific misconfigurations. The fix in version 2.0.8 hardens the trust evaluation logic to prevent IP address falsification in Express.js applications relying on this common middleware dependency.