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
Published September 7, 2026Reviewed 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).


Prevention & Best Practices

For Express.js Applications

  1. Separate authentication from authorization: Never assume authentication implies authorization for specific resources
  2. Use resource-specific middleware: Create reusable authorization middleware that receives (user, resource) and returns boolean decisions
  3. Implement object-level checks: For every route with :id parameters, verify the authenticated principal has access to that specific instance

Architectural Patterns

// Better pattern: database-level authorization
const budget = await Budget.findOne({
  where: { 
    syncId: req.params.budgetSyncId,
    ownerId: req.user.id  // implicit authorization
  }
});
if (!budget) return res.status(404).json({error: "Not found"});

Detection Tools

  • Semgrep: Rules like express-missing-authentication and custom IDOR patterns
  • CodeQL: Queries for "Uncontrolled data in path expression"
  • OWASP ZAP: Dynamic testing for forced browsing/BOLA

Standards & References


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.


References

Frequently Asked Questions

What is Broken Object-Level Authorization (BOLA)?

BOLA occurs when an API allows authenticated users to access objects (like database records) by modifying identifiers in requests, without verifying they own or have permission to access those specific objects.

How do you prevent BOLA in Express.js?

Always implement authorization checks after authentication, using middleware that validates the authenticated user's permissions against the specific resource being accessed. Use allowlists, database queries with user ID filters, or policy-based access control.

What CWE is Broken Object-Level Authorization?

CWE-639: Authorization Bypass by User-Controlled Key, though BOLA patterns also relate to CWE-284 (Improper Access Control) and CWE-285 (Improper Authorization).

Is API key validation enough to prevent BOLA?

No. API key validation only proves the requester is authenticated—it does not establish what resources they can access. Authorization must be checked separately for every object access.

Can static analysis detect BOLA?

Yes, advanced static analysis can detect missing authorization patterns by identifying routes that use user-controlled identifiers to access resources without intermediate authorization checks.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #112

Related Articles

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.

high

How Sandboxed Iframe Popup Restriction Bypass happens in Electron and how to fix it

A high-severity flaw in Electron (CVE-2026-70608) allowed sandboxed iframes to bypass the `allow-popups` sandbox restriction through the internal OpenURL navigation path, letting malicious or compromised embedded content spawn unauthorized popup windows. The fix upgrades Electron from 40.10.6 to 41.10.3 (also patched in 42.0.1 and 39.8.10), closing the navigation-layer gap without requiring any application code changes.

critical

How Missing Authentication on DELETE Endpoints Happens in Python aiohttp and How to Fix It

A critical missing authentication vulnerability in `pz_minimax.py` allowed any network-connected user to delete stored MiniMax prompts via the `DELETE /pz_easyuse/minimax-prompts/{index}` endpoint without any access control. An attacker could enumerate sequential indices to wipe all user-created prompts from the shared JSON file. The fix restricts the DELETE endpoint to localhost-only requests by checking `request.remote` against loopback addresses.

critical

How Information Disclosure Vulnerabilities Happen in Python APIs and How to Fix It

A critical information disclosure vulnerability in the Hermes plugin dashboard API was exposing sensitive filesystem paths, credential file locations, and configuration details without authentication. The fix redacts this sensitive information from API responses, replacing absolute paths with boolean status indicators to prevent attackers from locating and targeting credential files.

critical

How Missing Authentication on DELETE Endpoints Happens in Node.js Express and How to Fix It

A critical authentication bypass vulnerability was discovered in the skill-cabinet server where the DELETE /api/skills/:id endpoint allowed any unauthenticated user to delete arbitrary skills from the filesystem. The fix implements loopback origin validation to ensure only requests from localhost can perform destructive operations, while also consolidating delete functionality into a single, protected endpoint.

critical

How SQL Injection happens in PHP bulk email systems and how to fix it

A critical SQL injection vulnerability in `admin/utilities/bulkEmailSystem.php` allowed attackers to inject arbitrary SQL through unvalidated database names passed from user input. The fix implements strict input validation using regex pattern matching to ensure only safe database identifiers are processed, preventing exploitation of the bulk email functionality.