Parse Server GraphQL WebSocket endpoint bypasses security middleware
Description
Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Prior to 8.6.40 and 9.6.0-alpha.14, the GraphQL WebSocket endpoint for subscriptions does not pass requests through the Express middleware chain that enforces authentication, introspection control, and query complexity limits. An attacker can connect to the WebSocket endpoint and execute GraphQL operations without providing a valid application or API key, access the GraphQL schema via introspection even when public introspection is disabled, and send arbitrarily complex queries that bypass configured complexity limits. This vulnerability is fixed in 8.6.40 and 9.6.0-alpha.14.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
parse-servernpm | >= 9.0.0, < 9.6.0-alpha.14 | 9.6.0-alpha.14 |
parse-servernpm | < 8.6.40 | 8.6.40 |
Affected products
1- Range: >= 9.0.0 < 9.6.0-alpha.14
Patches
221330d146c68fix: GraphQL WebSocket endpoint bypasses security middleware ([GHSA-p2x3-8689-cwpg](https://github.com/parse-community/parse-server/security/advisories/GHSA-p2x3-8689-cwpg)) (#10190)
5 files changed · +146 −62
package.json+0 −1 modified@@ -56,7 +56,6 @@ "rate-limit-redis": "4.2.0", "redis": "4.7.0", "semver": "7.7.2", - "subscriptions-transport-ws": "0.11.0", "tv4": "1.3.0", "uuid": "11.1.0", "winston": "3.17.0",
package-lock.json+43 −8 modified@@ -46,7 +46,6 @@ "rate-limit-redis": "4.2.0", "redis": "4.7.0", "semver": "7.7.2", - "subscriptions-transport-ws": "0.11.0", "tv4": "1.3.0", "uuid": "11.1.0", "winston": "3.17.0", @@ -7896,7 +7895,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", @@ -10643,7 +10645,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", @@ -13276,7 +13281,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", @@ -21516,6 +21524,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", @@ -21531,6 +21542,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" } @@ -21539,6 +21553,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" }, @@ -28586,7 +28603,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", @@ -30513,7 +30533,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", @@ -32360,7 +32383,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", @@ -38115,6 +38141,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", @@ -38126,12 +38155,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": {} } }
spec/ParseGraphQLServer.spec.js+1 −34 modified@@ -8,16 +8,12 @@ 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 +54,6 @@ describe('ParseGraphQLServer', () => { parseGraphQLServer = new ParseGraphQLServer(parseServer, { graphQLPath: '/graphql', playgroundPath: '/playground', - subscriptionsPath: '/subscriptions', }); const logger = require('../lib/logger').default; @@ -241,16 +236,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 +452,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: {
spec/vulnerabilities.spec.js+101 −0 modified@@ -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', () => { @@ -2322,4 +2327,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).toMatch(/introspection/i); + }); + + 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/); + }); + }); });
src/GraphQL/ParseGraphQLServer.js+1 −19 modified@@ -4,8 +4,7 @@ import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled'; import express from 'express'; -import { execute, subscribe, GraphQLError } from 'graphql'; -import { SubscriptionServer } from 'subscriptions-transport-ws'; +import { GraphQLError } from 'graphql'; import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares'; import requiredParameter from '../requiredParameter'; import { createComplexityValidationPlugin } from './helpers/queryComplexity'; @@ -219,23 +218,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); }
3ffba757bfc8fix: GraphQL WebSocket endpoint bypasses security middleware ([GHSA-p2x3-8689-cwpg](https://github.com/parse-community/parse-server/security/advisories/GHSA-p2x3-8689-cwpg)) (#10189)
5 files changed · +146 −63
package.json+0 −1 modified@@ -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",
package-lock.json+43 −8 modified@@ -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": {} } }
spec/ParseGraphQLServer.spec.js+1 −35 modified@@ -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: {
spec/vulnerabilities.spec.js+101 −0 modified@@ -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/); + }); + }); });
src/GraphQL/ParseGraphQLServer.js+1 −19 modified@@ -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); }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-p2x3-8689-cwpgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-32594ghsaADVISORY
- github.com/parse-community/parse-server/commit/21330d146c68b57a930a58b8a8cd9fbf09436cf3ghsaWEB
- github.com/parse-community/parse-server/commit/3ffba757bfc836bd034e1369f4f64304e110e375ghsaWEB
- github.com/parse-community/parse-server/pull/10189ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/10190ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/security/advisories/GHSA-p2x3-8689-cwpgghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.