mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
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:
Generated
+43
-8
@@ -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": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user