Skip to content

Commit 0347641

Browse files
authored
fix: GraphQL API endpoint ignores CORS origin restriction ([GHSA-q3p6-g7c4-829c](GHSA-q3p6-g7c4-829c)) (#10335)
1 parent 9793e8f commit 0347641

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
@@ -4645,4 +4645,123 @@ describe('(GHSA-wp76-gg32-8258) /verifyPassword leaks raw authData via missing a
46454645
expect(response.data.authData?.mfa?.recovery).toBeUndefined();
46464646
expect(response.data.authData?.mfa).toEqual({ status: 'enabled' });
46474647
});
4648+
4649+
describe('(GHSA-q3p6-g7c4-829c) GraphQL endpoint ignores allowOrigin server option', () => {
4650+
let httpServer;
4651+
const gqlPort = 13398;
4652+
4653+
const gqlHeaders = {
4654+
'X-Parse-Application-Id': 'test',
4655+
'X-Parse-Javascript-Key': 'test',
4656+
'Content-Type': 'application/json',
4657+
};
4658+
4659+
async function setupGraphQLServer(serverOptions = {}) {
4660+
if (httpServer) {
4661+
await new Promise(resolve => httpServer.close(resolve));
4662+
}
4663+
const server = await reconfigureServer(serverOptions);
4664+
const expressApp = express();
4665+
httpServer = http.createServer(expressApp);
4666+
expressApp.use('/parse', server.app);
4667+
const parseGraphQLServer = new ParseGraphQLServer(server, {
4668+
graphQLPath: '/graphql',
4669+
});
4670+
parseGraphQLServer.applyGraphQL(expressApp);
4671+
await new Promise(resolve => httpServer.listen({ port: gqlPort }, resolve));
4672+
return parseGraphQLServer;
4673+
}
4674+
4675+
afterEach(async () => {
4676+
if (httpServer) {
4677+
await new Promise(resolve => httpServer.close(resolve));
4678+
httpServer = null;
4679+
}
4680+
});
4681+
4682+
it('should reflect allowed origin when allowOrigin is configured', async () => {
4683+
await setupGraphQLServer({ allowOrigin: 'https://example.com' });
4684+
const response = await fetch(`http://localhost:${gqlPort}/graphql`, {
4685+
method: 'POST',
4686+
headers: { ...gqlHeaders, Origin: 'https://example.com' },
4687+
body: JSON.stringify({ query: '{ health }' }),
4688+
});
4689+
expect(response.status).toBe(200);
4690+
expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com');
4691+
});
4692+
4693+
it('should not reflect unauthorized origin when allowOrigin is configured', async () => {
4694+
await setupGraphQLServer({ allowOrigin: 'https://example.com' });
4695+
const response = await fetch(`http://localhost:${gqlPort}/graphql`, {
4696+
method: 'POST',
4697+
headers: { ...gqlHeaders, Origin: 'https://unauthorized.example.net' },
4698+
body: JSON.stringify({ query: '{ health }' }),
4699+
});
4700+
expect(response.headers.get('access-control-allow-origin')).not.toBe('https://unauthorized.example.net');
4701+
expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com');
4702+
});
4703+
4704+
it('should support multiple allowed origins', async () => {
4705+
await setupGraphQLServer({ allowOrigin: ['https://a.example.com', 'https://b.example.com'] });
4706+
const responseA = await fetch(`http://localhost:${gqlPort}/graphql`, {
4707+
method: 'POST',
4708+
headers: { ...gqlHeaders, Origin: 'https://a.example.com' },
4709+
body: JSON.stringify({ query: '{ health }' }),
4710+
});
4711+
expect(responseA.headers.get('access-control-allow-origin')).toBe('https://a.example.com');
4712+
4713+
const responseB = await fetch(`http://localhost:${gqlPort}/graphql`, {
4714+
method: 'POST',
4715+
headers: { ...gqlHeaders, Origin: 'https://b.example.com' },
4716+
body: JSON.stringify({ query: '{ health }' }),
4717+
});
4718+
expect(responseB.headers.get('access-control-allow-origin')).toBe('https://b.example.com');
4719+
4720+
const responseUnauthorized = await fetch(`http://localhost:${gqlPort}/graphql`, {
4721+
method: 'POST',
4722+
headers: { ...gqlHeaders, Origin: 'https://unauthorized.example.net' },
4723+
body: JSON.stringify({ query: '{ health }' }),
4724+
});
4725+
expect(responseUnauthorized.headers.get('access-control-allow-origin')).not.toBe('https://unauthorized.example.net');
4726+
expect(responseUnauthorized.headers.get('access-control-allow-origin')).toBe('https://a.example.com');
4727+
});
4728+
4729+
it('should default to wildcard when allowOrigin is not configured', async () => {
4730+
await setupGraphQLServer();
4731+
const response = await fetch(`http://localhost:${gqlPort}/graphql`, {
4732+
method: 'POST',
4733+
headers: { ...gqlHeaders, Origin: 'https://example.com' },
4734+
body: JSON.stringify({ query: '{ health }' }),
4735+
});
4736+
expect(response.headers.get('access-control-allow-origin')).toBe('*');
4737+
});
4738+
4739+
it('should handle OPTIONS preflight with configured allowOrigin', async () => {
4740+
await setupGraphQLServer({ allowOrigin: 'https://example.com' });
4741+
const response = await fetch(`http://localhost:${gqlPort}/graphql`, {
4742+
method: 'OPTIONS',
4743+
headers: {
4744+
Origin: 'https://example.com',
4745+
'Access-Control-Request-Method': 'POST',
4746+
'Access-Control-Request-Headers': 'X-Parse-Application-Id, Content-Type',
4747+
},
4748+
});
4749+
expect(response.status).toBe(200);
4750+
expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com');
4751+
});
4752+
4753+
it('should not reflect unauthorized origin in OPTIONS preflight', async () => {
4754+
await setupGraphQLServer({ allowOrigin: 'https://example.com' });
4755+
const response = await fetch(`http://localhost:${gqlPort}/graphql`, {
4756+
method: 'OPTIONS',
4757+
headers: {
4758+
Origin: 'https://unauthorized.example.net',
4759+
'Access-Control-Request-Method': 'POST',
4760+
'Access-Control-Request-Headers': 'X-Parse-Application-Id, Content-Type',
4761+
},
4762+
});
4763+
expect(response.headers.get('access-control-allow-origin')).not.toBe('https://unauthorized.example.net');
4764+
expect(response.headers.get('access-control-allow-origin')).toBe('https://example.com');
4765+
});
4766+
});
46484767
});

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 '@apollo/server/express4';
54
import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled';
65
import express from 'express';
76
import { GraphQLError } from 'graphql';
8-
import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares';
7+
import { allowCrossDomain, handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares';
98
import requiredParameter from '../requiredParameter';
109
import { createComplexityValidationPlugin } from './helpers/queryComplexity';
1110
import defaultLogger from '../logger';
@@ -76,8 +75,7 @@ class ParseGraphQLServer {
7675
try {
7776
return {
7877
schema: await this.parseGraphQLSchema.load(),
79-
context: async ({ req, res }) => {
80-
res.set('access-control-allow-origin', req.get('origin') || '*');
78+
context: async ({ req }) => {
8179
return {
8280
info: req.info,
8381
config: req.config,
@@ -162,7 +160,7 @@ class ParseGraphQLServer {
162160
if (!app || !app.use) {
163161
requiredParameter('You must provide an Express.js app instance!');
164162
}
165-
app.use(this.config.graphQLPath, corsMiddleware());
163+
app.use(this.config.graphQLPath, allowCrossDomain(this.parseServer.config.appId));
166164
app.use(this.config.graphQLPath, handleParseHeaders);
167165
app.use(this.config.graphQLPath, handleParseSession);
168166
this.applyRequestContextMiddleware(app, this.parseServer.config);

0 commit comments

Comments
 (0)