fix: 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)

This commit is contained in:
Manuel
2026-03-27 15:04:01 +00:00
committed by GitHub
parent 9793e8fbb2
commit 0347641507
3 changed files with 124 additions and 7 deletions
+2 -2
View File
@@ -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;
});
+119
View File
@@ -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');
});
});
});
+3 -5
View File
@@ -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);