Broken Authentication

Broken Authentication and Node.js (Express) - CVE-2026-32594 [CVE-2026-32594]

[Fixed 2026-03] Updated CVE-2026-32594

Overview

The real-world impact of this vulnerability is the exposure of a critical authentication bypass in the GraphQL WebSocket subscription pathway of Parse Server when deployed on Node.js/Express. Before versions 8.6.40 and 9.6.0-alpha.14, the GraphQL WebSocket endpoint did not pass connections through the Express middleware chain that enforces authentication, introspection controls, and query complexity limits. An attacker could open a WebSocket connection to /subscriptions, perform GraphQL operations without a valid API key, enumerate the GraphQL schema via introspection even if public introspection was disabled, and submit arbitrarily complex queries that bypass configured complexity restrictions. This combination enables unauthorized data access and potential abuse of resources, highlighting a Broken Authentication weakness under CWE-306. The vulnerability is fixed in Parse Server 8.6.40 and 9.6.0-alpha.14, and the remediation requires aligning WebSocket subscription handling with the same authentication and authorization policies used for HTTP GraphQL endpoints. In Node.js/Express applications, ensure both HTTP and WebSocket GraphQL interfaces are secured consistently and that introspection and query complexity controls apply to both channels. This guide references CVE-2026-32594 to illustrate the concrete risk and fixes.

Affected Versions

Parse Server prior to 8.6.40 and 9.6.0-alpha.14

Code Fix Example

Node.js (Express) API Security Remediation
/* VULNERABLE PATTERN */
// Vulnerable code path: Express HTTP GraphQL and a separate WebSocket GraphQL subscription endpoint
const express = require('express');
const http = require('http');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const { PubSub } = require('graphql-subscriptions');

const app = express();
const server = http.createServer(app);
const pubsub = new PubSub();

// Simple schema with a subscription
const schema = buildSchema(`
  type Query { hello: String }
  type Subscription { count: Int! }
`);

// Minimal resolvers for demonstration (not fully wired in this snippet)
const root = {
  hello: () => 'Hello',
  // Subscription resolver would be wired via graphql-ws in a real setup
};

// HTTP GraphQL endpoint without authentication enforcement
app.use('/graphql', graphqlHTTP({ schema: schema, rootValue: root, graphiql: true }));

// WS GraphQL endpoint attached directly to server (bypasses Express middleware)
const wsServer = new WebSocketServer({ server, path: '/subscriptions' });
useServer({
  schema,
  // Vulnerable: no auth checks on WS connections
  onConnect: (ctx) => {
    // Accept any token or connectionParams; no validation performed
    return true;
  }
}, wsServer);

server.listen(4000, () => console.log('VULNERABLE server running at http://localhost:4000/graphql and ws://localhost:4000/subscriptions'));

/* FIXED PATTERN */
// Secure pattern: unify authentication across HTTP and WS, disable introspection in production, and enforce query complexity
const express2 = require('express');
const http2 = require('http');
const { graphqlHTTP: graphHTTP } = require('express-graphql');
const { buildSchema: buildSchema2 } = require('graphql');
const { WebSocketServer: WS2 } = require('ws');
const { useServer: useServer2 } = require('graphql-ws/lib/use/ws');
const { PubSub: PubSub2 } = require('graphql-subscriptions');

const appAuth = express2();
const serverAuth = http2.createServer(appAuth);
const pubsub2 = new PubSub2();

const schemaAuth = buildSchema2(`
  type Query { hello: String }
  type Subscription { count: Int! }
`);

// Implement a simple token verifier (replace with real JWT/audience checks in production)
function verifyToken(token) {
  // Example: token must be exactly 'secret-token' for demonstration
  return token === 'secret-token';
}

function extractTokenFromHeaders(req) {
  const auth = req.headers['authorization'];
  if (!auth) return null;
  return auth.startsWith('Bearer ') ? auth.slice(7) : auth;
}

function blockIntrospection(req, res, next) {
  if (req.method === 'POST' && req.body && typeof req.body.query === 'string') {
    const q = req.body.query;
    if (/__schema|__type/.test(q)) {
      return res.status(403).send('Introspection is disabled in this environment.');
    }
  }
  next();
}

// Enforce HTTP authentication for GraphQL HTTP endpoint
appAuth.use(express2.json());
appAuth.use('/graphql', (req, res, next) => {
  const token = extractTokenFromHeaders(req);
  if (!token || !verifyToken(token)) {
    return res.status(401).send('Unauthorized');
  }
  next();
}, blockIntrospection, graphHTTP({ schema: schemaAuth, rootValue: { hello: () => 'Hello' }, graphiql: false }));

// WS: enforce identical auth on connections
const wsServerAuth = new WS2({ server: serverAuth, path: '/subscriptions' });
useServer2({
  schema: schemaAuth,
  onConnect: (ctx) => {
    const { connectionParams } = ctx;
    const token = (connectionParams && (connectionParams.Authorization || connectionParams.authorization)) || null;
    if (!token || !verifyToken(token)) {
      throw new Error('Unauthorized');
    }
    return true;
  }
}, wsServerAuth);

// Optional: publish a simple counter to demonstrate activity with authentication in place
setInterval(() => pubsub2.publish('COUNT', { count: Math.floor(Math.random() * 100) }), 1000);

serverAuth.listen(5000, () => console.log('FIXED server running at http://localhost:5000/graphql and ws://localhost:5000/subscriptions'));

CVE References

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