Skip to content

Commit 4dd0d3d

Browse files
authored
fix: GraphQL API endpoint ignores CORS origin restriction ([GHSA-q3p6-g7c4-829c](GHSA-q3p6-g7c4-829c)) (#10334)
1 parent 5bb8ede commit 4dd0d3d

3 files changed

Lines changed: 124 additions & 7 deletions

File tree

spec/ParseGraphQLServer.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ describe('ParseGraphQLServer', () => {
503503
}
504504
});
505505

506-
it('should be cors enabled and scope the response within the source origin', async () => {
506+
it('should be cors enabled', async () => {
507507
let checked = false;
508508
const apolloClient = new ApolloClient({
509509
link: new ApolloLink((operation, forward) => {
@@ -512,7 +512,7 @@ describe('ParseGraphQLServer', () => {
512512
const {
513513
response: { headers },
514514
} = context;
515-
expect(headers.get('access-control-allow-origin')).toEqual('http://example.com');
515+
expect(headers.get('access-control-allow-origin')).toEqual('*');
516516
checked = true;
517517
return response;
518518
});

spec/vulnerabilities.spec.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5103,4 +5103,123 @@ describe('(GHSA-p2w6-rmh7-w8q3) SQL Injection via aggregate and distinct field n
51035103
expect(response.data.authData?.mfa).toEqual({ status: 'enabled' });
51045104
});
51055105
});
5106+
5107+
describe('(GHSA-q3p6-g7c4-829c) GraphQL endpoint ignores allowOrigin server option', () => {
5108+
let httpServer;
5109+
const gqlPort = 13398;
5110+
5111+
const gqlHeaders = {
5112+
'X-Parse-Application-Id': 'test',
5113+
'X-Parse-Javascript-Key': 'test',
5114+
'Content-Type': 'application/json',
5115+
};
5116+
5117+
async function setupGraphQLServer(serverOptions = {}) {
5118+
if (httpServer) {
5119+
await new Promise(resolve => httpServer.close(resolve));
5120+
}
5121+
const server = await reconfigureServer(serverOptions);
5122+
const expressApp = express();
5123+
httpServer = http.createServer(expressApp);
5124+
expressApp.use('/parse', server.app);
5125+
const parseGraphQLServer = new ParseGraphQLServer(server, {
5126+
graphQLPath: '/graphql',
5127+
});
5128+
parseGraphQLServer.applyGraphQL(expressApp);
5129+
await new Promise(resolve => httpServer.listen({ port: gqlPort }, resolve));
5130+
return parseGraphQLServer;
5131+
}
5132+
5133+
afterEach(async () => {
5134+
if (httpServer) {
5135+
await new Promise(resolve => httpServer.close(resolve));
5136+
httpServer = null;
5137+
}
5138+
});
5139+
5140+
it('should reflect allowed origin when allowOrigin is configured', async () => {
5141+
await setupGraphQLServer({ allowOrigin: 'https://example.com' });
5142+
const response = await fetch(`http://localhost:${gqlPort}/graphql`, {
5143+
method: 'POST',
5144+
headers: { ...gqlHeaders, Origin: 'https://example.com' },
5145+
body: JSON.stringify({ query: '{ health }' }),
5146+
});
5147+
expect(response.status).toBe(200);
5148+
expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com');
5149+
});
5150+
5151+
it('should not reflect unauthorized origin when allowOrigin is configured', async () => {
5152+
await setupGraphQLServer({ allowOrigin: 'https://example.com' });
5153+
const response = await fetch(`http://localhost:${gqlPort}/graphql`, {
5154+
method: 'POST',
5155+
headers: { ...gqlHeaders, Origin: 'https://unauthorized.example.net' },
5156+
body: JSON.stringify({ query: '{ health }' }),
5157+
});
5158+
expect(response.headers.get('access-control-allow-origin')).not.toBe('https://unauthorized.example.net');
5159+
expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com');
5160+
});
5161+
5162+
it('should support multiple allowed origins', async () => {
5163+
await setupGraphQLServer({ allowOrigin: ['https://a.example.com', 'https://b.example.com'] });
5164+
const responseA = await fetch(`http://localhost:${gqlPort}/graphql`, {
5165+
method: 'POST',
5166+
headers: { ...gqlHeaders, Origin: 'https://a.example.com' },
5167+
body: JSON.stringify({ query: '{ health }' }),
5168+
});
5169+
expect(responseA.headers.get('access-control-allow-origin')).toBe('https://a.example.com');
5170+
5171+
const responseB = await fetch(`http://localhost:${gqlPort}/graphql`, {
5172+
method: 'POST',
5173+
headers: { ...gqlHeaders, Origin: 'https://b.example.com' },
5174+
body: JSON.stringify({ query: '{ health }' }),
5175+
});
5176+
expect(responseB.headers.get('access-control-allow-origin')).toBe('https://b.example.com');
5177+
5178+
const responseUnauthorized = await fetch(`http://localhost:${gqlPort}/graphql`, {
5179+
method: 'POST',
5180+
headers: { ...gqlHeaders, Origin: 'https://unauthorized.example.net' },
5181+
body: JSON.stringify({ query: '{ health }' }),
5182+
});
5183+
expect(responseUnauthorized.headers.get('access-control-allow-origin')).not.toBe('https://unauthorized.example.net');
5184+
expect(responseUnauthorized.headers.get('access-control-allow-origin')).toBe('https://a.example.com');
5185+
});
5186+
5187+
it('should default to wildcard when allowOrigin is not configured', async () => {
5188+
await setupGraphQLServer();
5189+
const response = await fetch(`http://localhost:${gqlPort}/graphql`, {
5190+
method: 'POST',
5191+
headers: { ...gqlHeaders, Origin: 'https://example.com' },
5192+
body: JSON.stringify({ query: '{ health }' }),
5193+
});
5194+
expect(response.headers.get('access-control-allow-origin')).toBe('*');
5195+
});
5196+
5197+
it('should handle OPTIONS preflight with configured allowOrigin', async () => {
5198+
await setupGraphQLServer({ allowOrigin: 'https://example.com' });
5199+
const response = await fetch(`http://localhost:${gqlPort}/graphql`, {
5200+
method: 'OPTIONS',
5201+
headers: {
5202+
Origin: 'https://example.com',
5203+
'Access-Control-Request-Method': 'POST',
5204+
'Access-Control-Request-Headers': 'X-Parse-Application-Id, Content-Type',
5205+
},
5206+
});
5207+
expect(response.status).toBe(200);
5208+
expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com');
5209+
});
5210+
5211+
it('should not reflect unauthorized origin in OPTIONS preflight', async () => {
5212+
await setupGraphQLServer({ allowOrigin: 'https://example.com' });
5213+
const response = await fetch(`http://localhost:${gqlPort}/graphql`, {
5214+
method: 'OPTIONS',
5215+
headers: {
5216+
Origin: 'https://unauthorized.example.net',
5217+
'Access-Control-Request-Method': 'POST',
5218+
'Access-Control-Request-Headers': 'X-Parse-Application-Id, Content-Type',
5219+
},
5220+
});
5221+
expect(response.headers.get('access-control-allow-origin')).not.toBe('https://unauthorized.example.net');
5222+
expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com');
5223+
});
5224+
});
51065225
});

src/GraphQL/ParseGraphQLServer.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import corsMiddleware from 'cors';
21
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js';
32
import { ApolloServer } from '@apollo/server';
43
import { expressMiddleware } from '@as-integrations/express5';
54
import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled';
65
import express from 'express';
76
import { GraphQLError, parse } from 'graphql';
8-
import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares';
7+
import { allowCrossDomain, handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares';
98
import requiredParameter from '../requiredParameter';
109
import defaultLogger from '../logger';
1110
import { ParseGraphQLSchema } from './ParseGraphQLSchema';
@@ -116,8 +115,7 @@ class ParseGraphQLServer {
116115
try {
117116
return {
118117
schema: await this.parseGraphQLSchema.load(),
119-
context: async ({ req, res }) => {
120-
res.set('access-control-allow-origin', req.get('origin') || '*');
118+
context: async ({ req }) => {
121119
return {
122120
info: req.info,
123121
config: req.config,
@@ -204,7 +202,7 @@ class ParseGraphQLServer {
204202
if (!app || !app.use) {
205203
requiredParameter('You must provide an Express.js app instance!');
206204
}
207-
app.use(this.config.graphQLPath, corsMiddleware());
205+
app.use(this.config.graphQLPath, allowCrossDomain(this.parseServer.config.appId));
208206
app.use(this.config.graphQLPath, handleParseHeaders);
209207
app.use(this.config.graphQLPath, handleParseSession);
210208
this.applyRequestContextMiddleware(app, this.parseServer.config);

0 commit comments

Comments
 (0)