CVE-2026-34373
Description
Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Prior to versions 8.6.66 and 9.7.0-alpha.10, the GraphQL API endpoint does not respect the allowOrigin server option and unconditionally allows cross-origin requests from any website. This bypasses origin restrictions that operators configure to control which websites can interact with the Parse Server API. The REST API correctly enforces the configured allowOrigin restriction. This issue has been patched in versions 8.6.66 and 9.7.0-alpha.10.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
parse-servernpm | >= 9.0.0, < 9.7.0-alpha.10 | 9.7.0-alpha.10 |
parse-servernpm | >= 3.5.0, < 8.6.66 | 8.6.66 |
Affected products
10cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha1:*:*:*:node.js:*:*+ 9 more
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha1:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha2:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha3:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha4:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha5:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha6:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha7:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha8:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha9:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:*:*:*:*:*:node.js:*:*range: >=3.5.0,<8.6.66
Patches
2034764150789fix: GraphQL API endpoint ignores CORS origin restriction ([GHSA-q3p6-g7c4-829c](https://github.com/parse-community/parse-server/security/advisories/GHSA-q3p6-g7c4-829c)) (#10335)
3 files changed · +124 −7
spec/ParseGraphQLServer.spec.js+2 −2 modified@@ -503,7 +503,7 @@ describe('ParseGraphQLServer', () => { } }); - it('should be cors enabled and scope the response within the source origin', async () => { + it('should be cors enabled', async () => { let checked = false; const apolloClient = new ApolloClient({ link: new ApolloLink((operation, forward) => { @@ -512,7 +512,7 @@ describe('ParseGraphQLServer', () => { const { response: { headers }, } = context; - expect(headers.get('access-control-allow-origin')).toEqual('http://example.com'); + expect(headers.get('access-control-allow-origin')).toEqual('*'); checked = true; return response; });
spec/vulnerabilities.spec.js+119 −0 modified@@ -4645,4 +4645,123 @@ describe('(GHSA-wp76-gg32-8258) /verifyPassword leaks raw authData via missing a expect(response.data.authData?.mfa?.recovery).toBeUndefined(); expect(response.data.authData?.mfa).toEqual({ status: 'enabled' }); }); + + describe('(GHSA-q3p6-g7c4-829c) GraphQL endpoint ignores allowOrigin server option', () => { + let httpServer; + const gqlPort = 13398; + + const gqlHeaders = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + 'Content-Type': 'application/json', + }; + + async function setupGraphQLServer(serverOptions = {}) { + 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', + }); + parseGraphQLServer.applyGraphQL(expressApp); + await new Promise(resolve => httpServer.listen({ port: gqlPort }, resolve)); + return parseGraphQLServer; + } + + afterEach(async () => { + if (httpServer) { + await new Promise(resolve => httpServer.close(resolve)); + httpServer = null; + } + }); + + it('should reflect allowed origin when allowOrigin is configured', async () => { + await setupGraphQLServer({ allowOrigin: 'https://example.com' }); + const response = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'POST', + headers: { ...gqlHeaders, Origin: 'https://example.com' }, + body: JSON.stringify({ query: '{ health }' }), + }); + expect(response.status).toBe(200); + expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com'); + }); + + it('should not reflect unauthorized origin when allowOrigin is configured', async () => { + await setupGraphQLServer({ allowOrigin: 'https://example.com' }); + const response = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'POST', + headers: { ...gqlHeaders, Origin: 'https://unauthorized.example.net' }, + body: JSON.stringify({ query: '{ health }' }), + }); + expect(response.headers.get('access-control-allow-origin')).not.toBe('https://unauthorized.example.net'); + expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com'); + }); + + it('should support multiple allowed origins', async () => { + await setupGraphQLServer({ allowOrigin: ['https://a.example.com', 'https://b.example.com'] }); + const responseA = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'POST', + headers: { ...gqlHeaders, Origin: 'https://a.example.com' }, + body: JSON.stringify({ query: '{ health }' }), + }); + expect(responseA.headers.get('access-control-allow-origin')).toBe('https://a.example.com'); + + const responseB = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'POST', + headers: { ...gqlHeaders, Origin: 'https://b.example.com' }, + body: JSON.stringify({ query: '{ health }' }), + }); + expect(responseB.headers.get('access-control-allow-origin')).toBe('https://b.example.com'); + + const responseUnauthorized = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'POST', + headers: { ...gqlHeaders, Origin: 'https://unauthorized.example.net' }, + body: JSON.stringify({ query: '{ health }' }), + }); + expect(responseUnauthorized.headers.get('access-control-allow-origin')).not.toBe('https://unauthorized.example.net'); + expect(responseUnauthorized.headers.get('access-control-allow-origin')).toBe('https://a.example.com'); + }); + + it('should default to wildcard when allowOrigin is not configured', async () => { + await setupGraphQLServer(); + const response = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'POST', + headers: { ...gqlHeaders, Origin: 'https://example.com' }, + body: JSON.stringify({ query: '{ health }' }), + }); + expect(response.headers.get('access-control-allow-origin')).toBe('*'); + }); + + it('should handle OPTIONS preflight with configured allowOrigin', async () => { + await setupGraphQLServer({ allowOrigin: 'https://example.com' }); + const response = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'OPTIONS', + headers: { + Origin: 'https://example.com', + 'Access-Control-Request-Method': 'POST', + 'Access-Control-Request-Headers': 'X-Parse-Application-Id, Content-Type', + }, + }); + expect(response.status).toBe(200); + expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com'); + }); + + it('should not reflect unauthorized origin in OPTIONS preflight', async () => { + await setupGraphQLServer({ allowOrigin: 'https://example.com' }); + const response = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'OPTIONS', + headers: { + Origin: 'https://unauthorized.example.net', + 'Access-Control-Request-Method': 'POST', + 'Access-Control-Request-Headers': 'X-Parse-Application-Id, Content-Type', + }, + }); + expect(response.headers.get('access-control-allow-origin')).not.toBe('https://unauthorized.example.net'); + expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com'); + }); + }); });
src/GraphQL/ParseGraphQLServer.js+3 −5 modified@@ -1,11 +1,10 @@ -import corsMiddleware from 'cors'; import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js'; import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled'; import express from 'express'; import { GraphQLError } from 'graphql'; -import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares'; +import { allowCrossDomain, handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares'; import requiredParameter from '../requiredParameter'; import { createComplexityValidationPlugin } from './helpers/queryComplexity'; import defaultLogger from '../logger'; @@ -76,8 +75,7 @@ class ParseGraphQLServer { try { return { schema: await this.parseGraphQLSchema.load(), - context: async ({ req, res }) => { - res.set('access-control-allow-origin', req.get('origin') || '*'); + context: async ({ req }) => { return { info: req.info, config: req.config, @@ -162,7 +160,7 @@ class ParseGraphQLServer { if (!app || !app.use) { requiredParameter('You must provide an Express.js app instance!'); } - app.use(this.config.graphQLPath, corsMiddleware()); + app.use(this.config.graphQLPath, allowCrossDomain(this.parseServer.config.appId)); app.use(this.config.graphQLPath, handleParseHeaders); app.use(this.config.graphQLPath, handleParseSession); this.applyRequestContextMiddleware(app, this.parseServer.config);
4dd0d3d8be1cfix: GraphQL API endpoint ignores CORS origin restriction ([GHSA-q3p6-g7c4-829c](https://github.com/parse-community/parse-server/security/advisories/GHSA-q3p6-g7c4-829c)) (#10334)
3 files changed · +124 −7
spec/ParseGraphQLServer.spec.js+2 −2 modified@@ -503,7 +503,7 @@ describe('ParseGraphQLServer', () => { } }); - it('should be cors enabled and scope the response within the source origin', async () => { + it('should be cors enabled', async () => { let checked = false; const apolloClient = new ApolloClient({ link: new ApolloLink((operation, forward) => { @@ -512,7 +512,7 @@ describe('ParseGraphQLServer', () => { const { response: { headers }, } = context; - expect(headers.get('access-control-allow-origin')).toEqual('http://example.com'); + expect(headers.get('access-control-allow-origin')).toEqual('*'); checked = true; return response; });
spec/vulnerabilities.spec.js+119 −0 modified@@ -5103,4 +5103,123 @@ describe('(GHSA-p2w6-rmh7-w8q3) SQL Injection via aggregate and distinct field n expect(response.data.authData?.mfa).toEqual({ status: 'enabled' }); }); }); + + describe('(GHSA-q3p6-g7c4-829c) GraphQL endpoint ignores allowOrigin server option', () => { + let httpServer; + const gqlPort = 13398; + + const gqlHeaders = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + 'Content-Type': 'application/json', + }; + + async function setupGraphQLServer(serverOptions = {}) { + 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', + }); + parseGraphQLServer.applyGraphQL(expressApp); + await new Promise(resolve => httpServer.listen({ port: gqlPort }, resolve)); + return parseGraphQLServer; + } + + afterEach(async () => { + if (httpServer) { + await new Promise(resolve => httpServer.close(resolve)); + httpServer = null; + } + }); + + it('should reflect allowed origin when allowOrigin is configured', async () => { + await setupGraphQLServer({ allowOrigin: 'https://example.com' }); + const response = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'POST', + headers: { ...gqlHeaders, Origin: 'https://example.com' }, + body: JSON.stringify({ query: '{ health }' }), + }); + expect(response.status).toBe(200); + expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com'); + }); + + it('should not reflect unauthorized origin when allowOrigin is configured', async () => { + await setupGraphQLServer({ allowOrigin: 'https://example.com' }); + const response = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'POST', + headers: { ...gqlHeaders, Origin: 'https://unauthorized.example.net' }, + body: JSON.stringify({ query: '{ health }' }), + }); + expect(response.headers.get('access-control-allow-origin')).not.toBe('https://unauthorized.example.net'); + expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com'); + }); + + it('should support multiple allowed origins', async () => { + await setupGraphQLServer({ allowOrigin: ['https://a.example.com', 'https://b.example.com'] }); + const responseA = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'POST', + headers: { ...gqlHeaders, Origin: 'https://a.example.com' }, + body: JSON.stringify({ query: '{ health }' }), + }); + expect(responseA.headers.get('access-control-allow-origin')).toBe('https://a.example.com'); + + const responseB = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'POST', + headers: { ...gqlHeaders, Origin: 'https://b.example.com' }, + body: JSON.stringify({ query: '{ health }' }), + }); + expect(responseB.headers.get('access-control-allow-origin')).toBe('https://b.example.com'); + + const responseUnauthorized = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'POST', + headers: { ...gqlHeaders, Origin: 'https://unauthorized.example.net' }, + body: JSON.stringify({ query: '{ health }' }), + }); + expect(responseUnauthorized.headers.get('access-control-allow-origin')).not.toBe('https://unauthorized.example.net'); + expect(responseUnauthorized.headers.get('access-control-allow-origin')).toBe('https://a.example.com'); + }); + + it('should default to wildcard when allowOrigin is not configured', async () => { + await setupGraphQLServer(); + const response = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'POST', + headers: { ...gqlHeaders, Origin: 'https://example.com' }, + body: JSON.stringify({ query: '{ health }' }), + }); + expect(response.headers.get('access-control-allow-origin')).toBe('*'); + }); + + it('should handle OPTIONS preflight with configured allowOrigin', async () => { + await setupGraphQLServer({ allowOrigin: 'https://example.com' }); + const response = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'OPTIONS', + headers: { + Origin: 'https://example.com', + 'Access-Control-Request-Method': 'POST', + 'Access-Control-Request-Headers': 'X-Parse-Application-Id, Content-Type', + }, + }); + expect(response.status).toBe(200); + expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com'); + }); + + it('should not reflect unauthorized origin in OPTIONS preflight', async () => { + await setupGraphQLServer({ allowOrigin: 'https://example.com' }); + const response = await fetch(`http://localhost:${gqlPort}/graphql`, { + method: 'OPTIONS', + headers: { + Origin: 'https://unauthorized.example.net', + 'Access-Control-Request-Method': 'POST', + 'Access-Control-Request-Headers': 'X-Parse-Application-Id, Content-Type', + }, + }); + expect(response.headers.get('access-control-allow-origin')).not.toBe('https://unauthorized.example.net'); + expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com'); + }); + }); });
src/GraphQL/ParseGraphQLServer.js+3 −5 modified@@ -1,11 +1,10 @@ -import corsMiddleware from 'cors'; import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js'; import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@as-integrations/express5'; import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled'; import express from 'express'; import { GraphQLError, parse } from 'graphql'; -import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares'; +import { allowCrossDomain, handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares'; import requiredParameter from '../requiredParameter'; import defaultLogger from '../logger'; import { ParseGraphQLSchema } from './ParseGraphQLSchema'; @@ -116,8 +115,7 @@ class ParseGraphQLServer { try { return { schema: await this.parseGraphQLSchema.load(), - context: async ({ req, res }) => { - res.set('access-control-allow-origin', req.get('origin') || '*'); + context: async ({ req }) => { return { info: req.info, config: req.config, @@ -204,7 +202,7 @@ class ParseGraphQLServer { if (!app || !app.use) { requiredParameter('You must provide an Express.js app instance!'); } - app.use(this.config.graphQLPath, corsMiddleware()); + app.use(this.config.graphQLPath, allowCrossDomain(this.parseServer.config.appId)); app.use(this.config.graphQLPath, handleParseHeaders); app.use(this.config.graphQLPath, handleParseSession); this.applyRequestContextMiddleware(app, this.parseServer.config);
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/parse-community/parse-server/commit/0347641507891d0013ec57f7c10f012064f41263nvdPatchWEB
- github.com/parse-community/parse-server/commit/4dd0d3d8be1c39664c74ad10bb0abaa76bc41203nvdPatchWEB
- github.com/parse-community/parse-server/pull/10334nvdIssue TrackingPatchWEB
- github.com/parse-community/parse-server/pull/10335nvdIssue TrackingPatchWEB
- github.com/parse-community/parse-server/security/advisories/GHSA-q3p6-g7c4-829cnvdPatchVendor AdvisoryWEB
- github.com/advisories/GHSA-q3p6-g7c4-829cghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-34373ghsaADVISORY
News mentions
0No linked articles in our index yet.