mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
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:
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user