Back to Blog
critical SEVERITY6 min read

How missing authorization enforcement happens in Node.js Express routers and how to fix it

A critical authorization bypass was discovered in lib/router.js where readOnly and noDelete configuration options were only enforced through UI controls, not server-side middleware. Any authenticated user could bypass these restrictions by sending direct HTTP requests to perform destructive operations like database deletion or document modification. The fix adds Express middleware that enforces these security modes at the server level, blocking POST, PUT, and DELETE requests when appropriate.

O
By Orbis AppSec
Published July 31, 2026Reviewed July 31, 2026

Answer Summary

This vulnerability is a missing authorization enforcement issue (CWE-862) in a Node.js Express router where security controls (readOnly and noDelete modes) were only enforced client-side through hidden UI elements. Attackers could bypass these by sending direct HTTP requests. The fix adds server-side middleware in lib/router.js that checks the request method against configuration flags and redirects unauthorized operations before they reach route handlers.

Vulnerability at a Glance

cweCWE-862 (Missing Authorization)
fixAdded Express middleware to enforce readOnly and noDelete modes server-side
riskAny authenticated user can delete databases, export sensitive data, or modify documents
languageNode.js / Express.js
root causeSecurity controls only enforced in UI templates, not in backend request handlers
vulnerabilityMissing Server-Side Authorization Enforcement

Introduction

In the lib/router.js file of a MongoDB administration interface, we discovered a critical authorization bypass that could allow any authenticated user to perform destructive database operations. The application had readOnly and noDelete configuration options designed to protect production databases, but these controls were only enforced through hidden UI elements—not at the server level.

This meant that while the web interface would hide delete buttons and edit forms when readOnly was enabled, an attacker could simply send a DELETE /db/production_db request directly and completely bypass these protections. The vulnerable pattern existed around line 331 in router.js, where route handlers processed requests without validating the application's security configuration.

The Vulnerability Explained

The core issue was a classic client-side security anti-pattern: trusting the UI to enforce authorization decisions. The application's config.options.readOnly and config.options.noDelete flags were used by templates to conditionally render controls, but the actual route handlers never checked these flags.

Here's what the request flow looked like before the fix:

HTTP Request  Express Router  Route Handler  Database Operation
                                  No authorization check for readOnly/noDelete

The templates would hide the "Delete Database" button when readOnly was true, but nothing stopped this direct request:

# Attacker bypasses UI restrictions entirely
curl -X DELETE https://admin.example.com/db/production_db \
  -H "Cookie: session=authenticated_user_session"

Real-World Attack Scenarios

Scenario 1: Production Database Deletion
An operator configures the admin interface with readOnly: true to let developers view production data safely. A malicious or compromised developer account sends DELETE /db/production_db, wiping the entire production database.

Scenario 2: Sensitive Data Exfiltration
With noDelete enabled but no read restrictions, an attacker sends GET /db/production_db/users/export to bulk-export sensitive user data, bypassing any UI-level export restrictions.

Scenario 3: Document Tampering
An authenticated user with view-only intentions sends PUT /db/production_db/users/doc/admin_user_id to modify administrative records, escalating their privileges or corrupting critical data.

The vulnerability is particularly severe because the application is explicitly designed for database administration—the very operations it exposes are inherently destructive.

The Fix

The fix introduces Express middleware that enforces readOnly and noDelete at the server level, before requests ever reach route handlers:

/*
 * Server-side enforcement of readOnly and noDelete. The templates hide the controls and
 * two collection handlers check the flags, but nothing stopped a direct HTTP request, so
 * a DELETE still removed the document with readOnly turned on.
 *
 * Redirects go to the configured base URL rather than res.locals.baseHref: the latter is
 * derived from req.originalUrl, and CodeQL flags feeding request-controlled data into
 * res.redirect. config.site.baseUrl is server configuration the caller cannot influence.
 */
appRouter.use(function (req, res, next) {
  const safeRedirect = () => res.redirect(config.site.baseUrl || '/');

  if (config.options.readOnly && ['POST', 'PUT', 'DELETE'].includes(req.method)) {
    req.session.error = 'Application is running in read-only mode!';
    return safeRedirect();
  }
  if (config.options.noDelete && req.method === 'DELETE') {
    req.session.error = 'Delete operations are not permitted!';
    return safeRedirect();
  }
  next();
});

Key Security Improvements

1. Server-Side Enforcement: The middleware runs before any route handler, ensuring that no code path can bypass the check.

2. Method-Based Blocking: When readOnly is enabled, all state-changing methods (POST, PUT, DELETE) are blocked. When only noDelete is set, just DELETE requests are blocked.

3. Safe Redirect Pattern: The redirect uses config.site.baseUrl (server configuration) rather than req.originalUrl (user-controlled input), preventing a secondary open redirect vulnerability that CodeQL would flag.

4. User Feedback: The middleware sets req.session.error to inform users why their action was blocked, rather than silently failing.

Request Flow After Fix

HTTP Request  Express Router  Authorization Middleware  Route Handler  Database Operation
                                                                      Checks readOnly/noDelete
                              Redirects if unauthorized

Prevention & Best Practices

1. Never Trust Client-Side Controls

Any security control that only exists in the UI can be bypassed. Always implement authorization checks server-side:

// BAD: Only hiding the button
if (!config.options.readOnly) {
  res.render('admin', { showDeleteButton: true });
}

// GOOD: Server-side enforcement
app.use((req, res, next) => {
  if (config.options.readOnly && isWriteMethod(req.method)) {
    return res.status(403).json({ error: 'Read-only mode' });
  }
  next();
});

2. Use Middleware for Cross-Cutting Concerns

Authorization that applies to multiple routes should be implemented as middleware, not duplicated in each handler:

// Centralized authorization middleware
const enforceReadOnly = (req, res, next) => {
  if (config.readOnly && ['POST', 'PUT', 'DELETE'].includes(req.method)) {
    return res.status(403).send('Forbidden');
  }
  next();
};

// Apply to all routes
app.use(enforceReadOnly);

3. Test Authorization Independently

The new test file test/lib/routers/readOnlySpec.js demonstrates testing authorization separately from UI behavior:

// Test that direct HTTP requests are blocked
it('should block DELETE when readOnly is enabled', async () => {
  const response = await supertest(app)
    .delete('/db/test_db')
    .set('Cookie', sessionCookie);

  expect(response.status).to.equal(302); // Redirected
  // Verify database still exists
});

4. Defense in Depth

Even with middleware enforcement, consider:
- Database-level permissions (MongoDB roles)
- Network segmentation (admin interface on internal network only)
- Audit logging for all destructive operations
- Rate limiting on sensitive endpoints

Key Takeaways

  • UI controls are not security controls: The readOnly template logic created a false sense of security while leaving the actual API unprotected
  • Middleware placement matters: The authorization middleware was added before route definitions, ensuring it runs first for all requests
  • Redirect URLs must use trusted sources: Using config.site.baseUrl instead of req.originalUrl prevents introducing an open redirect vulnerability in the fix
  • Test the bypass, not just the feature: Security tests should verify that direct HTTP requests are blocked, not just that UI elements are hidden
  • Configuration options need enforcement code: Having a readOnly config option is meaningless without code that actually enforces it

How Orbis AppSec Detected This

  • Source: Authenticated HTTP requests reaching route handlers in lib/router.js
  • Sink: Database operation endpoints (DELETE, PUT, POST handlers) that execute without checking config.options.readOnly or config.options.noDelete
  • Missing control: No server-side middleware enforcing the readOnly and noDelete configuration flags before route handlers execute
  • CWE: CWE-862 (Missing Authorization)
  • Fix: Added Express middleware at line 331 that checks request method against configuration flags and redirects unauthorized operations with appropriate error messages

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

This vulnerability demonstrates a fundamental security principle: authorization must be enforced at the point of action, not just at the point of display. The readOnly and noDelete options gave operators confidence that their databases were protected, but that protection evaporated the moment someone used curl instead of a web browser.

The fix is elegant in its simplicity—a single middleware function that checks two configuration flags. But its placement in the request pipeline, before any route handler, is what makes it effective. When implementing similar protection in your own applications, remember: if an HTTP request can reach your server, assume someone will send it directly, regardless of what your UI allows.

References

Frequently Asked Questions

What is missing authorization enforcement?

Missing authorization enforcement occurs when security controls exist in configuration or UI but aren't validated on the server, allowing attackers to bypass restrictions via direct API requests.

How do you prevent missing authorization in Node.js Express?

Always implement authorization checks in server-side middleware that executes before route handlers, never rely solely on UI controls to hide or disable functionality.

What CWE is missing authorization enforcement?

CWE-862 (Missing Authorization) covers cases where software does not perform authorization checks when an actor attempts to access a resource or perform an action.

Is hiding UI controls enough to prevent unauthorized actions?

No, hiding UI controls is never sufficient security. Attackers can easily bypass client-side restrictions using tools like curl, Postman, or browser developer tools to send direct HTTP requests.

Can static analysis detect missing authorization?

Yes, static analysis tools can detect missing authorization by identifying routes that lack middleware checks or by comparing configuration-defined restrictions against actual enforcement points.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1900

Related Articles

critical

How command injection happens in Java Runtime.exec() and how to fix it

A critical command injection vulnerability was discovered in `ProcessUtils.java` where process IDs were concatenated directly into shell command strings passed to `Runtime.getRuntime().exec(String)`. This allowed shell metacharacter injection that could execute arbitrary commands. The fix switches to the array-based `exec(String[])` overload, which bypasses shell interpretation entirely.

critical

How Algolia API key exposure happens in EJS templates and how to fix it

A critical vulnerability in `layout/_plugins/global/config.ejs` was exposing Algolia API credentials — including `appId` and `apiKey` — directly in rendered HTML, making them visible to any user who inspects the page source. The fix removes raw credential interpolation from the template and replaces unsafe string embedding with properly sanitized output using `JSON.stringify()`. This eliminates the risk of unauthorized Algolia API access by anyone who visits the page.

high

How unauthenticated endpoint exposure happens in Node.js Express and how to fix it

A high-severity vulnerability in the AgenticATODetectionService allowed unauthenticated users to access sensitive agent status data, trigger detection scans, and view security alerts. The fix adds authentication middleware to four critical API endpoints, ensuring only authorized users can access these sensitive operations.

critical

How unsigned auto-update code execution happens in Node.js Neutralinojs and how to fix it

A critical vulnerability in the WeekBox application's self-update mechanism allowed attackers to serve malicious binaries through man-in-the-middle attacks or repository compromise. The `app-updater.service.js` file downloaded and installed updates from GitHub Releases without enforcing cryptographic hash verification before proceeding with the update. The fix adds mandatory SHA-256 digest validation that halts the update process if a valid hash is not present in the release metadata.

critical

How command injection via shell metacharacter escaping happens in Node.js and how to fix it

A critical command injection vulnerability was discovered in the GameBanana provider module where the `quoteCommandArgument()` function only escaped double quotes, leaving shell metacharacters like `$()`, backticks, and other dangerous patterns exploitable. Attackers could craft malicious mod URLs on GameBanana containing shell commands that would execute when users viewed the content. The fix switches from double-quote to single-quote escaping, which prevents shell interpretation of metacharact

critical

How SQL injection happens in PHP MySQLi and how to fix it

A critical SQL injection vulnerability was discovered in `sign_up.php` where user registration inputs—including Username and Email—were directly concatenated into SQL queries. Despite using `mysqli_real_escape_string()`, the code remained exploitable. The fix replaces all string-concatenated queries with MySQLi prepared statements and bound parameters, completely eliminating the injection vector.