Broken Object Property Level Authorization

Broken Object Property Level Authorization in Node.js [CVE-2026-33888]

[Fixed April 2026] Updated CVE-2026-33888

Overview

Broken Object Property Level Authorization vulnerabilities occur when an API allows a client to influence which fields are returned from a data store before the server has validated the caller’s permissions. In real-world Node.js (Express) apps, an attacker can append a project or projection parameter to a REST API request, which is then used to compose a MongoDB projection. If the code applies this client-supplied projection before performing authorization checks, the admin-configured public projection (publicApiProjection) can be bypassed or skipped, unintentionally exposing fields that administrators intended to keep private (such as internal notes, draft content, or metadata). The CVE-2026-33888 example in ApostropheCMS shows this pattern: versions 4.28.0 and earlier contained an authorization bypass in the getRestQuery method of the @apostrophecms/piece-type module, where an unauthenticated attacker could supply a project query parameter and pre-populate the projection state, causing the publicApiProjection to be skipped entirely. This resulted in disclosure of fields that should be restricted. The vulnerability was fixed in 4.29.0. In Node.js (Express) apps, a similar flaw can manifest if projections are built from client input before authorization, leading to Broken Object Property Level Authorization (CWE-200, CWE-863) and information exposure. The remediation is to enforce authentication and authorization before applying any client-provided projections, and to use a strict server-side projection policy with a field whitelist.

Affected Versions

4.28.0 and earlier

Code Fix Example

Node.js (Express) API Security Remediation
/* Vulnerable version (no auth check, uses client-supplied projection) */
const express = require('express');
const { MongoClient } = require('mongodb');
const app = express();

const MONGO_URL = 'mongodb://localhost:27017';
const DB_NAME = 'mydb';
let db;

const adminPublicProjection = { title: 1, summary: 1, date: 1 };

MongoClient.connect(MONGO_URL, { useNewUrlParser: true, useUnifiedTopology: true }, (err, client) => {
  if (err) throw err;
  db = client.db(DB_NAME);
  app.listen(3000, () => console.log('Vulnerable server running'));
});

// Vulnerable endpoint: applies client-supplied projection before any auth checks
app.get('/docs/vulnerable', (req, res) => {
  let projection = {};
  if (req.query.project) {
     try {
        projection = JSON.parse(req.query.project);
     } catch (e) {
        return res.status(400).json({ error: 'Invalid projection' });
     }
  }
  // If no projection is provided, apply the public projection
  if (Object.keys(projection).length === 0) {
     projection = adminPublicProjection;
  }
  db.collection('documents').find({}, projection).toArray((err, docs) => {
     if (err) return res.status(500).json({ error: 'DB error' });
     res.json(docs);
  });
});

// Fixed endpoint: require auth first, then apply a whitelist to the projection
app.get('/docs/fixed', (req, res) => {
  // Simple auth placeholder; in real apps replace with proper auth middleware
  if (!req.user) return res.status(401).json({ error: 'Unauthorized' });

  const whitelist = ['title', 'summary', 'date', 'author'];
  const requested = (req.query.project || '').split(',').map(s => s.trim()).filter(Boolean);
  const extra = {};
  requested.forEach(f => { if (whitelist.includes(f)) extra[f] = 1; });

  const projection = { ...adminPublicProjection, ...extra };

  db.collection('documents').find({}, projection).toArray((err, docs) => {
     if (err) return res.status(500).json({ error: 'DB error' });
     res.json(docs);
  });
});

const port = process.env.PORT || 3000;
app.listen(port, () => console.log('App listening on port ' + port));

CVE References

Choose which optional cookies to allow. You can change this any time.