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
readOnlytemplate 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.baseUrlinstead ofreq.originalUrlprevents 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
readOnlyconfig 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.readOnlyorconfig.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.