Skip to content

Commit 3ffba75

Browse files
authored
fix: GraphQL WebSocket endpoint bypasses security middleware ([GHSA-p2x3-8689-cwpg](GHSA-p2x3-8689-cwpg)) (#10189)
1 parent 26109e9 commit 3ffba75

File tree

5 files changed

+146
-63
lines changed

5 files changed

+146
-63
lines changed

package-lock.json

Lines changed: 43 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757
"rate-limit-redis": "4.2.0",
5858
"redis": "5.10.0",
5959
"semver": "7.7.2",
60-
"subscriptions-transport-ws": "0.11.0",
6160
"tv4": "1.3.0",
6261
"uuid": "11.1.0",
6362
"winston": "3.19.0",

spec/ParseGraphQLServer.spec.js

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,16 @@ const express = require('express');
33
const req = require('../lib/request');
44
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
55
const FormData = require('form-data');
6-
const ws = require('ws');
76
require('./helper');
87
const { updateCLP } = require('./support/dev');
98

109
const pluralize = require('pluralize');
11-
const { getMainDefinition } = require('@apollo/client/utilities');
1210
const createUploadLink = (...args) => import('apollo-upload-client/createUploadLink.mjs').then(({ default: fn }) => fn(...args));
13-
const { SubscriptionClient } = require('subscriptions-transport-ws');
14-
const { WebSocketLink } = require('@apollo/client/link/ws');
1511
const { mergeSchemas } = require('@graphql-tools/schema');
1612
const {
1713
ApolloClient,
1814
InMemoryCache,
1915
ApolloLink,
20-
split,
2116
createHttpLink,
2217
} = require('@apollo/client/core');
2318
const gql = require('graphql-tag');
@@ -58,7 +53,6 @@ describe('ParseGraphQLServer', () => {
5853
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
5954
graphQLPath: '/graphql',
6055
playgroundPath: '/playground',
61-
subscriptionsPath: '/subscriptions',
6256
});
6357

6458
const logger = require('../lib/logger').default;
@@ -241,16 +235,6 @@ describe('ParseGraphQLServer', () => {
241235
});
242236
});
243237

244-
describe('createSubscriptions', () => {
245-
it('should require initialization with config.subscriptionsPath', () => {
246-
expect(() =>
247-
new ParseGraphQLServer(parseServer, {
248-
graphQLPath: 'graphql',
249-
}).createSubscriptions({})
250-
).toThrow('You must provide a config.subscriptionsPath to createSubscriptions!');
251-
});
252-
});
253-
254238
describe('setGraphQLConfig', () => {
255239
let parseGraphQLServer;
256240
beforeEach(() => {
@@ -467,41 +451,23 @@ describe('ParseGraphQLServer', () => {
467451
parseGraphQLServer = new ParseGraphQLServer(_parseServer, {
468452
graphQLPath: '/graphql',
469453
playgroundPath: '/playground',
470-
subscriptionsPath: '/subscriptions',
471454
...parseGraphQLServerOptions,
472455
});
473456
parseGraphQLServer.applyGraphQL(expressApp);
474457
parseGraphQLServer.applyPlayground(expressApp);
475-
parseGraphQLServer.createSubscriptions(httpServer);
476458
await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve));
477459
}
478460

479461
beforeEach(async () => {
480462
await createGQLFromParseServer(parseServer);
481463

482-
const subscriptionClient = new SubscriptionClient(
483-
'ws://localhost:13377/subscriptions',
484-
{
485-
reconnect: true,
486-
connectionParams: headers,
487-
},
488-
ws
489-
);
490-
const wsLink = new WebSocketLink(subscriptionClient);
491464
const httpLink = await createUploadLink({
492465
uri: 'http://localhost:13377/graphql',
493466
fetch,
494467
headers,
495468
});
496469
apolloClient = new ApolloClient({
497-
link: split(
498-
({ query }) => {
499-
const { kind, operation } = getMainDefinition(query);
500-
return kind === 'OperationDefinition' && operation === 'subscription';
501-
},
502-
wsLink,
503-
httpLink
504-
),
470+
link: httpLink,
505471
cache: new InMemoryCache(),
506472
defaultOptions: {
507473
query: {

spec/vulnerabilities.spec.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
const http = require('http');
2+
const express = require('express');
3+
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
4+
const ws = require('ws');
15
const request = require('../lib/request');
26
const Config = require('../lib/Config');
7+
const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer');
38

49
describe('Vulnerabilities', () => {
510
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
24562461
expect(userB.id).toBeDefined();
24572462
});
24582463
});
2464+
2465+
describe('(GHSA-p2x3-8689-cwpg) GraphQL WebSocket middleware bypass', () => {
2466+
let httpServer;
2467+
const gqlPort = 13399;
2468+
2469+
const gqlHeaders = {
2470+
'X-Parse-Application-Id': 'test',
2471+
'X-Parse-Javascript-Key': 'test',
2472+
'Content-Type': 'application/json',
2473+
};
2474+
2475+
async function setupGraphQLServer(serverOptions = {}, graphQLOptions = {}) {
2476+
if (httpServer) {
2477+
await new Promise(resolve => httpServer.close(resolve));
2478+
}
2479+
const server = await reconfigureServer(serverOptions);
2480+
const expressApp = express();
2481+
httpServer = http.createServer(expressApp);
2482+
expressApp.use('/parse', server.app);
2483+
const parseGraphQLServer = new ParseGraphQLServer(server, {
2484+
graphQLPath: '/graphql',
2485+
...graphQLOptions,
2486+
});
2487+
parseGraphQLServer.applyGraphQL(expressApp);
2488+
await new Promise(resolve => httpServer.listen({ port: gqlPort }, resolve));
2489+
return parseGraphQLServer;
2490+
}
2491+
2492+
async function gqlRequest(query, headers = gqlHeaders) {
2493+
const response = await fetch(`http://localhost:${gqlPort}/graphql`, {
2494+
method: 'POST',
2495+
headers,
2496+
body: JSON.stringify({ query }),
2497+
});
2498+
return { status: response.status, body: await response.json().catch(() => null) };
2499+
}
2500+
2501+
afterEach(async () => {
2502+
if (httpServer) {
2503+
await new Promise(resolve => httpServer.close(resolve));
2504+
httpServer = null;
2505+
}
2506+
});
2507+
2508+
it('should not have createSubscriptions method', async () => {
2509+
const pgServer = await setupGraphQLServer();
2510+
expect(pgServer.createSubscriptions).toBeUndefined();
2511+
});
2512+
2513+
it('should not accept WebSocket connections on /subscriptions path', async () => {
2514+
await setupGraphQLServer();
2515+
const connectionResult = await new Promise((resolve) => {
2516+
const socket = new ws(`ws://localhost:${gqlPort}/subscriptions`);
2517+
socket.on('open', () => {
2518+
socket.close();
2519+
resolve('connected');
2520+
});
2521+
socket.on('error', () => {
2522+
resolve('refused');
2523+
});
2524+
setTimeout(() => {
2525+
socket.close();
2526+
resolve('timeout');
2527+
}, 2000);
2528+
});
2529+
expect(connectionResult).not.toBe('connected');
2530+
});
2531+
2532+
it('HTTP GraphQL should still work with API key', async () => {
2533+
await setupGraphQLServer();
2534+
const result = await gqlRequest('{ health }');
2535+
expect(result.status).toBe(200);
2536+
expect(result.body?.data?.health).toBeTruthy();
2537+
});
2538+
2539+
it('HTTP GraphQL should still reject requests without API key', async () => {
2540+
await setupGraphQLServer();
2541+
const result = await gqlRequest('{ health }', { 'Content-Type': 'application/json' });
2542+
expect(result.status).toBe(403);
2543+
});
2544+
2545+
it('HTTP introspection control should still work', async () => {
2546+
await setupGraphQLServer({}, { graphQLPublicIntrospection: false });
2547+
const result = await gqlRequest('{ __schema { types { name } } }');
2548+
expect(result.body?.errors).toBeDefined();
2549+
expect(result.body.errors[0].message).toContain('Introspection is not allowed');
2550+
});
2551+
2552+
it('HTTP complexity limits should still work', async () => {
2553+
await setupGraphQLServer({ requestComplexity: { graphQLFields: 5 } });
2554+
const fields = Array.from({ length: 10 }, (_, i) => `f${i}: health`).join(' ');
2555+
const result = await gqlRequest(`{ ${fields} }`);
2556+
expect(result.body?.errors).toBeDefined();
2557+
expect(result.body.errors[0].message).toMatch(/exceeds maximum allowed/);
2558+
});
2559+
});
24592560
});

src/GraphQL/ParseGraphQLServer.js

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import { ApolloServer } from '@apollo/server';
44
import { expressMiddleware } from '@as-integrations/express5';
55
import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled';
66
import express from 'express';
7-
import { execute, subscribe, GraphQLError, parse } from 'graphql';
8-
import { SubscriptionServer } from 'subscriptions-transport-ws';
7+
import { GraphQLError, parse } from 'graphql';
98
import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares';
109
import requiredParameter from '../requiredParameter';
1110
import defaultLogger from '../logger';
@@ -261,23 +260,6 @@ class ParseGraphQLServer {
261260
);
262261
}
263262

264-
createSubscriptions(server) {
265-
SubscriptionServer.create(
266-
{
267-
execute,
268-
subscribe,
269-
onOperation: async (_message, params, webSocket) =>
270-
Object.assign({}, params, await this._getGraphQLOptions(webSocket.upgradeReq)),
271-
},
272-
{
273-
server,
274-
path:
275-
this.config.subscriptionsPath ||
276-
requiredParameter('You must provide a config.subscriptionsPath to createSubscriptions!'),
277-
}
278-
);
279-
}
280-
281263
setGraphQLConfig(graphQLConfig: ParseGraphQLConfig): Promise {
282264
return this.parseGraphQLController.updateGraphQLConfig(graphQLConfig);
283265
}

0 commit comments

Comments
 (0)