Broken Access Control in Express
How Broken Access Control Manifests in Express
Broken Access Control in Express applications typically emerges through several Express-specific patterns. The most common is improper route protection where middleware like authenticate() is applied to some routes but not others, creating gaps in authorization. For instance, an admin dashboard might be protected while the underlying data modification endpoints remain exposed:
app.get('/admin', authenticate, (req, res) => { res.render('dashboard'); });
app.put('/users/:id', (req, res) => { updateUser(req.params.id, req.body); }); // Missing authAnother Express-specific manifestation is parameter pollution through URL parameters. Express's flexible routing can lead to confusion between route parameters and query parameters:
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
const override = req.query.id; // Can override route param
// Attacker can use /users/123?id=456 to access wrong user
});Express's middleware stack ordering creates additional vulnerabilities. Middleware order matters significantly:
// Vulnerable: auth middleware after the route
app.get('/profile', (req, res) => {
res.json({ data: getUser(req.query.id || req.user.id) });
});
app.use(authenticate); // Too late!Object-level authorization bypasses occur when developers rely on client-side filtering instead of server-side checks:
app.get('/api/posts', (req, res) => {
const posts = db.getAllPosts();
const filtered = posts.filter(p => p.authorId === req.user.id);
res.json(filtered); // Client can still request specific IDs
});Express's flexible parameter handling enables IDOR attacks through multiple parameter sources:
app.put('/api/items/:itemId', (req, res) => {
const itemId = req.params.itemId;
const overrideId = req.body.itemId; // Can override URL param
updateItem(overrideId, req.body); // Uses body param instead!
});Express-Specific Detection
Detecting broken access control in Express requires understanding its routing patterns and middleware architecture. Manual detection starts with mapping your middleware stack:
// Map all routes and their middleware
const routes = app._router.stack
.filter(r => r.route)
.map(r => ({
path: r.route.path,
methods: Object.keys(r.route.methods),
middleware: r.route.stack.map(m => m.name)
}));Automated scanning with middleBrick specifically targets Express patterns. The scanner identifies unprotected routes by analyzing the middleware chain and detecting routes missing authentication/authorization middleware. It tests parameter manipulation by sending requests with modified URL parameters, query parameters, and body parameters to verify if authorization checks are properly enforced.
middleBrick's Express-specific detection includes:
- Parameter pollution testing - modifying route parameters through query/body overrides
- Middleware bypass attempts - testing if authentication can be circumvented through timing or ordering
- Object-level authorization verification - attempting to access resources belonging to other users
- Method override testing - using HTTP methods like POST with X-HTTP-Method-Override headers
The scanner also analyzes Express's error handling patterns, as improper error responses can leak information about resource existence:
app.get('/api/users/:id', (req, res) => {
const user = db.findUser(req.params.id);
if (!user) {
res.status(404).json({ error: 'User not found' }); // Leaks existence
}
});middleBrick's continuous monitoring (Pro plan) can track changes to your Express routes over time, alerting when new endpoints appear without proper authorization checks or when existing routes have their middleware stack modified.
Express-Specific Remediation
Express provides several native patterns for fixing broken access control. The most effective is consistent middleware application using route-specific middleware:
// Create reusable auth middleware
const requireAuth = (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
next();
};
// Apply consistently to all protected routes
app.use('/api/admin*', requireAuth);
app.use('/api/users/:id', requireAuth, checkResourceOwnership);
// Object-level authorization middleware
const checkResourceOwnership = async (req, res, next) => {
const resource = await getResource(req.params.id);
if (resource.ownerId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Access denied' });
}
next();
};
// Use Express Router for scoped middleware
const userRouter = express.Router();
userRouter.use(requireAuth);
userRouter.get('/:id', checkResourceOwnership, getUser);
userRouter.put('/:id', checkResourceOwnership, updateUser);
app.use('/api/users', userRouter);Express's parameter handling can be secured through explicit parameter validation:
app.put('/api/items/:itemId', requireAuth, async (req, res) => {
const itemId = req.params.itemId;
// Prevent parameter pollution
if (req.body.itemId && req.body.itemId !== itemId) {
return res.status(400).json({ error: 'Parameter mismatch' });
}
const item = await getItem(itemId);
if (!item) return res.status(404).json({ error: 'Not found' });
// Check ownership
if (item.ownerId !== req.user.id) {
return res.status(403).json({ error: 'Access denied' });
}
await updateItem(itemId, req.body);
res.json({ success: true });
});For comprehensive protection, implement a centralized authorization service using Express's context:
app.use(async (req, res, next) => {
req.authz = new AuthorizationService(req.user);
next();
});
// Resource-specific authorization
const requireOwnership = (resourceType) => {
return async (req, res, next) => {
const resource = await db[resourceType].findById(req.params.id);
if (!resource) return res.status(404).json({ error: 'Not found' });
if (!await req.authz.canAccess(resourceType, resource.id)) {
return res.status(403).json({ error: 'Access denied' });
}
req.resource = resource;
next();
};
};
app.get('/api/posts/:id', requireAuth, requireOwnership('posts'), (req, res) => {
res.json(req.resource);
});middleBrick's CLI tool can be integrated into your Express development workflow to continuously scan for broken access control as you develop:
npx middlebrick scan http://localhost:3000/api --watch
# Automatically scans on file changesThe GitHub Action integration allows you to fail builds when broken access control is detected:
name: API Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run middleBrick Scan
run: |
npx middlebrick scan https://staging.example.com/api \
--threshold B \
--fail-below B
continue-on-error: false