HIGH broken access controlexpress

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 auth

Another 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 changes

The 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

Frequently Asked Questions

How does Express's flexible routing contribute to broken access control?
Express's flexible routing allows multiple ways to specify the same endpoint, creating authorization gaps. A route defined as '/users/:id' can be accessed with various parameter combinations, and if authorization checks only validate one parameter source, attackers can bypass them by using alternative parameter locations like query strings or request bodies.
Can middleBrick detect broken access control in my Express API?
Yes, middleBrick specifically tests Express applications for broken access control by analyzing your middleware stack, testing parameter manipulation across all input sources, and attempting to access resources across user boundaries. It identifies unprotected routes and verifies that authorization checks are properly enforced for all resource access patterns.