Skip to content

Commit 0ae0ed3

Browse files
authored
test: GraphQL endpoint is exempt from routeAllowList by design (parse-community#10480)
1 parent 828d0e0 commit 0ae0ed3

5 files changed

Lines changed: 74 additions & 5 deletions

File tree

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ A big _thank you_ 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
7373
- [Restricting File URL Domains](#restricting-file-url-domains)
7474
- [Idempotency Enforcement](#idempotency-enforcement)
7575
- [Installations](#installations)
76+
- [Options](#options)
77+
- [`duplicateDeviceTokenActionEnforceAuth`](#duplicatedevicetokenactionenforceauth)
78+
- [`duplicateDeviceTokenAction`](#duplicatedevicetokenaction)
79+
- [`duplicateDeviceTokenMergePriority`](#duplicatedevicetokenmergepriority)
80+
- [Configuration example](#configuration-example)
7681
- [Localization](#localization)
7782
- [Pages](#pages)
7883
- [Localization with Directory Structure](#localization-with-directory-structure)
@@ -314,7 +319,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
314319
315320
## Route Allow List
316321

317-
The `routeAllowList` option restricts which API routes are accessible to external clients. When set, all external requests are denied by default unless the route matches one of the configured regex patterns. This is useful for apps where all logic runs in Cloud Code and clients should not access the API directly.
322+
The `routeAllowList` option restricts which REST API routes are accessible to external clients. When set, all external REST API requests are denied by default unless the route matches one of the configured regex patterns. This is useful for apps where all logic runs in Cloud Code and clients should not access the REST API directly.
318323

319324
Internal calls from Cloud Code, Cloud Jobs, and triggers are not affected. Master key and maintenance key requests bypass the restriction.
320325

@@ -334,7 +339,7 @@ const server = ParseServer({
334339

335340
Each entry is a regex pattern matched against the normalized route identifier. Patterns are auto-anchored with `^` and `$` for full-match semantics. For example, `classes/Chat` matches only `classes/Chat`, not `classes/ChatRoom`. Use `classes/Chat.*` to match both.
336341

337-
Setting an empty array `[]` blocks all external non-master-key requests (full lockdown). Not setting the option preserves current behavior (all routes accessible).
342+
Setting an empty array `[]` blocks all external non-master-key REST API requests (full lockdown of REST API routes). Not setting the option preserves current behavior (all routes accessible).
338343

339344
### Covered Routes
340345

@@ -395,6 +400,9 @@ The following table lists all route groups covered by `routeAllowList` with exam
395400
> [!NOTE]
396401
> File routes are not covered by `routeAllowList`. File upload access is controlled via the `fileUpload` option. File download and metadata access is controlled via the `fileDownload` option.
397402
403+
> [!NOTE]
404+
> The GraphQL API is not covered by `routeAllowList`. `routeAllowList` gates the REST API per route, while every GraphQL operation is transported over a single endpoint with the operation, target class, and field set encoded in the request body — so per-route allow-list semantics do not compose with it.
405+
398406
## Email Verification and Password Reset
399407

400408
Verifying user email addresses and enabling password reset via email requires an email adapter. There are many email adapters provided and maintained by the community. The following is an example configuration with an example email adapter. See the [Parse Server Options][server-options] for more details and a full list of available options.

spec/RouteAllowList.spec.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,67 @@ describe('routeAllowList', () => {
314314
}
315315
});
316316

317+
describe('GraphQL exemption', () => {
318+
// routeAllowList is a path-based REST API control. The GraphQL endpoint
319+
// collapses every operation onto a single URL (graphQLPath), so a
320+
// per-route allow-list cannot meaningfully gate individual GraphQL
321+
// operations.
322+
const gqlRequest = body =>
323+
require('../lib/request')({
324+
method: 'POST',
325+
url: 'http://localhost:8378/graphql',
326+
headers: {
327+
'Content-Type': 'application/json',
328+
'X-Parse-Application-Id': 'test',
329+
'X-Parse-Javascript-Key': 'test',
330+
},
331+
body: JSON.stringify(body),
332+
});
333+
334+
it('reaches GraphQL endpoint when routeAllowList is empty array', async () => {
335+
await reconfigureServer({ mountGraphQL: true, routeAllowList: [] });
336+
const restRequest = require('../lib/request');
337+
await expectAsync(
338+
restRequest({
339+
method: 'GET',
340+
url: 'http://localhost:8378/1/classes/GameScore',
341+
headers: {
342+
'X-Parse-Application-Id': 'test',
343+
'X-Parse-REST-API-Key': 'rest',
344+
},
345+
})
346+
).toBeRejectedWith(
347+
jasmine.objectContaining({
348+
data: jasmine.objectContaining({ code: Parse.Error.OPERATION_FORBIDDEN }),
349+
})
350+
);
351+
const response = await gqlRequest({ query: '{ health }' });
352+
expect(response.data.data.health).toBeTrue();
353+
});
354+
355+
it('reaches GraphQL endpoint when routeAllowList contains only REST routes', async () => {
356+
await reconfigureServer({
357+
mountGraphQL: true,
358+
routeAllowList: ['classes/AllowedClass'],
359+
});
360+
const response = await gqlRequest({ query: '{ health }' });
361+
expect(response.data.data.health).toBeTrue();
362+
});
363+
364+
it('keeps class CLP enforced through GraphQL when routeAllowList is empty array', async () => {
365+
await reconfigureServer({ mountGraphQL: true, routeAllowList: [] });
366+
const { updateCLP } = require('./support/dev');
367+
const obj = new Parse.Object('CLPGuarded');
368+
await obj.save(null, { useMasterKey: true });
369+
await updateCLP({ find: {}, get: {}, create: {}, update: {}, delete: {} }, 'CLPGuarded');
370+
const response = await gqlRequest({
371+
query: '{ cLPGuardeds { edges { node { objectId } } } }',
372+
});
373+
expect(response.data.errors).toBeDefined();
374+
expect(response.data.errors[0].extensions.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
375+
});
376+
});
377+
317378
it_id('229cab22-dad3-4d08-8de5-64d813658596')(it)('should block all route groups when not in allow list', async () => {
318379
await reconfigureServer({
319380
routeAllowList: ['classes/GameScore'],

src/Options/Definitions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ module.exports.ParseServerOptions = {
583583
},
584584
routeAllowList: {
585585
env: 'PARSE_SERVER_ROUTE_ALLOW_LIST',
586-
help: '(Optional) Restricts external client access to a list of allowed API routes.<br><br>When this option is set, all external non-master-key requests are denied by default. Only routes matching at least one of the configured regex patterns are allowed through. Internal calls from Cloud Code, Cloud Jobs, and triggers are not affected.<br><br>Each entry is a regex pattern string matched against the normalized route identifier (request path with mount prefix and leading slash stripped). Patterns are auto-anchored with `^` and `$` for full-match semantics.<br><br><b>Examples of normalized route identifiers:</b><ul><li>`classes/GameScore` (class CRUD)</li><li>`classes/GameScore/abc123` (object by ID)</li><li>`users` (user operations)</li><li>`login` (login endpoint)</li><li>`functions/sendEmail` (Cloud Function)</li><li>`jobs/cleanup` (Cloud Job)</li><li>`push` (push notifications)</li><li>`config` (client config)</li><li>`installations` (installations)</li><li>`files/picture.jpg` (file operations)</li></ul><b>Example patterns:</b><ul><li>`classes/ChatMessage` matches only `classes/ChatMessage`</li><li>`classes/Chat.*` matches `classes/ChatMessage`, `classes/ChatRoom`, etc.</li><li>`functions/.*` matches all Cloud Functions</li></ul>Setting an empty array `[]` blocks all external non-master-key requests (full lockdown).<br><br>When setting the option via an environment variable, the notation is a comma-separated string, for example `"classes/ChatMessage,users,functions/.*"`.<br><br>Defaults to `undefined` which means the feature is inactive and all routes are accessible.',
586+
help: '(Optional) Restricts external client access to a list of allowed REST API routes.<br><br>When this option is set, all external non-master-key REST API requests are denied by default. Only routes matching at least one of the configured regex patterns are allowed through. Internal calls from Cloud Code, Cloud Jobs, and triggers are not affected.<br><br>Each entry is a regex pattern string matched against the normalized route identifier (request path with mount prefix and leading slash stripped). Patterns are auto-anchored with `^` and `$` for full-match semantics.<br><br><b>Examples of normalized route identifiers:</b><ul><li>`classes/GameScore` (class CRUD)</li><li>`classes/GameScore/abc123` (object by ID)</li><li>`users` (user operations)</li><li>`login` (login endpoint)</li><li>`functions/sendEmail` (Cloud Function)</li><li>`jobs/cleanup` (Cloud Job)</li><li>`push` (push notifications)</li><li>`config` (client config)</li><li>`installations` (installations)</li></ul><b>Example patterns:</b><ul><li>`classes/ChatMessage` matches only `classes/ChatMessage`</li><li>`classes/Chat.*` matches `classes/ChatMessage`, `classes/ChatRoom`, etc.</li><li>`functions/.*` matches all Cloud Functions</li></ul>Setting an empty array `[]` blocks all external non-master-key REST API requests (full lockdown of REST API routes).<br><br>When setting the option via an environment variable, the notation is a comma-separated string, for example `"classes/ChatMessage,users,functions/.*"`.<br><br>Defaults to `undefined` which means the feature is inactive and all routes are accessible.<br><br><b>Note:</b> File routes and the GraphQL API are not covered by this option.',
587587
action: parsers.arrayParser,
588588
},
589589
scheduledPush: {

src/Options/docs.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)