fix: GraphQL WebSocket endpoint bypasses security middleware ([GHSA-p2x3-8689-cwpg](https://github.com/parse-community/parse-server/security/advisories/GHSA-p2x3-8689-cwpg)) (#10189)

This commit is contained in:
Manuel
2026-03-12 14:23:50 +00:00
committed by GitHub
parent 26109e9230
commit 3ffba757bf
5 changed files with 146 additions and 63 deletions
+43 -8
View File
@@ -47,7 +47,6 @@
"rate-limit-redis": "4.2.0",
"redis": "5.10.0",
"semver": "7.7.2",
"subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0",
"uuid": "11.1.0",
"winston": "3.19.0",
@@ -7552,7 +7551,10 @@
"node_modules/backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
"integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA=="
"integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/backoff": {
"version": "2.5.0",
@@ -10256,7 +10258,10 @@
"node_modules/eventemitter3": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/execa": {
"version": "5.1.1",
@@ -12843,7 +12848,10 @@
"node_modules/iterall": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz",
"integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg=="
"integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/jackspeak": {
"version": "3.4.3",
@@ -21058,6 +21066,9 @@
"resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz",
"integrity": "sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==",
"deprecated": "The `subscriptions-transport-ws` package is no longer maintained. We recommend you use `graphql-ws` instead. For help migrating Apollo software to `graphql-ws`, see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws For general help using `graphql-ws`, see https://github.com/enisdenjo/graphql-ws/blob/master/README.md",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"backo2": "^1.0.2",
"eventemitter3": "^3.1.0",
@@ -21073,6 +21084,9 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -21081,6 +21095,9 @@
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=8.3.0"
},
@@ -27987,7 +28004,10 @@
"backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
"integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA=="
"integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==",
"dev": true,
"optional": true,
"peer": true
},
"backoff": {
"version": "2.5.0",
@@ -29877,7 +29897,10 @@
"eventemitter3": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==",
"dev": true,
"optional": true,
"peer": true
},
"execa": {
"version": "5.1.1",
@@ -31673,7 +31696,10 @@
"iterall": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz",
"integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg=="
"integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==",
"dev": true,
"optional": true,
"peer": true
},
"jackspeak": {
"version": "3.4.3",
@@ -37372,6 +37398,9 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz",
"integrity": "sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==",
"dev": true,
"optional": true,
"peer": true,
"requires": {
"backo2": "^1.0.2",
"eventemitter3": "^3.1.0",
@@ -37383,12 +37412,18 @@
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
"dev": true,
"optional": true,
"peer": true
},
"ws": {
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"dev": true,
"optional": true,
"peer": true,
"requires": {}
}
}
-1
View File
@@ -57,7 +57,6 @@
"rate-limit-redis": "4.2.0",
"redis": "5.10.0",
"semver": "7.7.2",
"subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0",
"uuid": "11.1.0",
"winston": "3.19.0",
+1 -35
View File
@@ -3,21 +3,16 @@ const express = require('express');
const req = require('../lib/request');
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
const FormData = require('form-data');
const ws = require('ws');
require('./helper');
const { updateCLP } = require('./support/dev');
const pluralize = require('pluralize');
const { getMainDefinition } = require('@apollo/client/utilities');
const createUploadLink = (...args) => import('apollo-upload-client/createUploadLink.mjs').then(({ default: fn }) => fn(...args));
const { SubscriptionClient } = require('subscriptions-transport-ws');
const { WebSocketLink } = require('@apollo/client/link/ws');
const { mergeSchemas } = require('@graphql-tools/schema');
const {
ApolloClient,
InMemoryCache,
ApolloLink,
split,
createHttpLink,
} = require('@apollo/client/core');
const gql = require('graphql-tag');
@@ -58,7 +53,6 @@ describe('ParseGraphQLServer', () => {
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
graphQLPath: '/graphql',
playgroundPath: '/playground',
subscriptionsPath: '/subscriptions',
});
const logger = require('../lib/logger').default;
@@ -241,16 +235,6 @@ describe('ParseGraphQLServer', () => {
});
});
describe('createSubscriptions', () => {
it('should require initialization with config.subscriptionsPath', () => {
expect(() =>
new ParseGraphQLServer(parseServer, {
graphQLPath: 'graphql',
}).createSubscriptions({})
).toThrow('You must provide a config.subscriptionsPath to createSubscriptions!');
});
});
describe('setGraphQLConfig', () => {
let parseGraphQLServer;
beforeEach(() => {
@@ -467,41 +451,23 @@ describe('ParseGraphQLServer', () => {
parseGraphQLServer = new ParseGraphQLServer(_parseServer, {
graphQLPath: '/graphql',
playgroundPath: '/playground',
subscriptionsPath: '/subscriptions',
...parseGraphQLServerOptions,
});
parseGraphQLServer.applyGraphQL(expressApp);
parseGraphQLServer.applyPlayground(expressApp);
parseGraphQLServer.createSubscriptions(httpServer);
await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve));
}
beforeEach(async () => {
await createGQLFromParseServer(parseServer);
const subscriptionClient = new SubscriptionClient(
'ws://localhost:13377/subscriptions',
{
reconnect: true,
connectionParams: headers,
},
ws
);
const wsLink = new WebSocketLink(subscriptionClient);
const httpLink = await createUploadLink({
uri: 'http://localhost:13377/graphql',
fetch,
headers,
});
apolloClient = new ApolloClient({
link: split(
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
httpLink
),
link: httpLink,
cache: new InMemoryCache(),
defaultOptions: {
query: {
+101
View File
@@ -1,5 +1,10 @@
const http = require('http');
const express = require('express');
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
const ws = require('ws');
const request = require('../lib/request');
const Config = require('../lib/Config');
const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer');
describe('Vulnerabilities', () => {
describe('(GHSA-8xq9-g7ch-35hg) Custom object ID allows to acquire role privilege', () => {
@@ -2456,4 +2461,100 @@ describe('(GHSA-c442-97qw-j6c6) SQL Injection via $regex query operator field na
expect(userB.id).toBeDefined();
});
});
describe('(GHSA-p2x3-8689-cwpg) GraphQL WebSocket middleware bypass', () => {
let httpServer;
const gqlPort = 13399;
const gqlHeaders = {
'X-Parse-Application-Id': 'test',
'X-Parse-Javascript-Key': 'test',
'Content-Type': 'application/json',
};
async function setupGraphQLServer(serverOptions = {}, graphQLOptions = {}) {
if (httpServer) {
await new Promise(resolve => httpServer.close(resolve));
}
const server = await reconfigureServer(serverOptions);
const expressApp = express();
httpServer = http.createServer(expressApp);
expressApp.use('/parse', server.app);
const parseGraphQLServer = new ParseGraphQLServer(server, {
graphQLPath: '/graphql',
...graphQLOptions,
});
parseGraphQLServer.applyGraphQL(expressApp);
await new Promise(resolve => httpServer.listen({ port: gqlPort }, resolve));
return parseGraphQLServer;
}
async function gqlRequest(query, headers = gqlHeaders) {
const response = await fetch(`http://localhost:${gqlPort}/graphql`, {
method: 'POST',
headers,
body: JSON.stringify({ query }),
});
return { status: response.status, body: await response.json().catch(() => null) };
}
afterEach(async () => {
if (httpServer) {
await new Promise(resolve => httpServer.close(resolve));
httpServer = null;
}
});
it('should not have createSubscriptions method', async () => {
const pgServer = await setupGraphQLServer();
expect(pgServer.createSubscriptions).toBeUndefined();
});
it('should not accept WebSocket connections on /subscriptions path', async () => {
await setupGraphQLServer();
const connectionResult = await new Promise((resolve) => {
const socket = new ws(`ws://localhost:${gqlPort}/subscriptions`);
socket.on('open', () => {
socket.close();
resolve('connected');
});
socket.on('error', () => {
resolve('refused');
});
setTimeout(() => {
socket.close();
resolve('timeout');
}, 2000);
});
expect(connectionResult).not.toBe('connected');
});
it('HTTP GraphQL should still work with API key', async () => {
await setupGraphQLServer();
const result = await gqlRequest('{ health }');
expect(result.status).toBe(200);
expect(result.body?.data?.health).toBeTruthy();
});
it('HTTP GraphQL should still reject requests without API key', async () => {
await setupGraphQLServer();
const result = await gqlRequest('{ health }', { 'Content-Type': 'application/json' });
expect(result.status).toBe(403);
});
it('HTTP introspection control should still work', async () => {
await setupGraphQLServer({}, { graphQLPublicIntrospection: false });
const result = await gqlRequest('{ __schema { types { name } } }');
expect(result.body?.errors).toBeDefined();
expect(result.body.errors[0].message).toContain('Introspection is not allowed');
});
it('HTTP complexity limits should still work', async () => {
await setupGraphQLServer({ requestComplexity: { graphQLFields: 5 } });
const fields = Array.from({ length: 10 }, (_, i) => `f${i}: health`).join(' ');
const result = await gqlRequest(`{ ${fields} }`);
expect(result.body?.errors).toBeDefined();
expect(result.body.errors[0].message).toMatch(/exceeds maximum allowed/);
});
});
});
+1 -19
View File
@@ -4,8 +4,7 @@ import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@as-integrations/express5';
import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled';
import express from 'express';
import { execute, subscribe, GraphQLError, parse } from 'graphql';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { GraphQLError, parse } from 'graphql';
import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares';
import requiredParameter from '../requiredParameter';
import defaultLogger from '../logger';
@@ -261,23 +260,6 @@ class ParseGraphQLServer {
);
}
createSubscriptions(server) {
SubscriptionServer.create(
{
execute,
subscribe,
onOperation: async (_message, params, webSocket) =>
Object.assign({}, params, await this._getGraphQLOptions(webSocket.upgradeReq)),
},
{
server,
path:
this.config.subscriptionsPath ||
requiredParameter('You must provide a config.subscriptionsPath to createSubscriptions!'),
}
);
}
setGraphQLConfig(graphQLConfig: ParseGraphQLConfig): Promise {
return this.parseGraphQLController.updateGraphQLConfig(graphQLConfig);
}