Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ A big _thank you_ 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
- [Restricting File URL Domains](#restricting-file-url-domains)
- [Idempotency Enforcement](#idempotency-enforcement)
- [Installations](#installations)
- [Options](#options)
- [`duplicateDeviceTokenActionEnforceAuth`](#duplicatedevicetokenactionenforceauth)
- [`duplicateDeviceTokenAction`](#duplicatedevicetokenaction)
- [`duplicateDeviceTokenMergePriority`](#duplicatedevicetokenmergepriority)
- [Configuration example](#configuration-example)
- [Localization](#localization)
- [Pages](#pages)
- [Localization with Directory Structure](#localization-with-directory-structure)
Expand Down Expand Up @@ -314,7 +319,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo

## Route Allow List

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.
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.

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

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

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.

Setting an empty array `[]` blocks all external non-master-key requests (full lockdown). Not setting the option preserves current behavior (all routes accessible).
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).

### Covered Routes

Expand Down Expand Up @@ -395,6 +400,9 @@ The following table lists all route groups covered by `routeAllowList` with exam
> [!NOTE]
> 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.

> [!NOTE]
> 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.

## Email Verification and Password Reset

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.
Expand Down
21 changes: 21 additions & 0 deletions changelogs/CHANGELOG_alpha.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
## [9.9.1-alpha.3](https://github.com/parse-community/parse-server/compare/9.9.1-alpha.2...9.9.1-alpha.3) (2026-05-27)


### Bug Fixes

* Server option routeAllowList is bypassable through batch sub-requests ([GHSA-p84r-h6rx-f2xr](https://github.com/parse-community/parse-server/security/advisories/GHSA-p84r-h6rx-f2xr)) ([#10482](https://github.com/parse-community/parse-server/issues/10482)) ([552c6dd](https://github.com/parse-community/parse-server/commit/552c6dd754638c9f546fbceecd2ba0f7225a95d1))

## [9.9.1-alpha.2](https://github.com/parse-community/parse-server/compare/9.9.1-alpha.1...9.9.1-alpha.2) (2026-05-18)


### Bug Fixes

* GraphQL "Did you mean" validation suggestions disclose schema to unauthenticated callers ([GHSA-8cph-rgr4-g5vj](https://github.com/parse-community/parse-server/security/advisories/GHSA-8cph-rgr4-g5vj)) ([#10467](https://github.com/parse-community/parse-server/issues/10467)) ([155123a](https://github.com/parse-community/parse-server/commit/155123ade9bc88cdf4807cf267ea1196f9274773))

## [9.9.1-alpha.1](https://github.com/parse-community/parse-server/compare/9.9.0...9.9.1-alpha.1) (2026-05-17)


### Bug Fixes

* Pre-authentication denial of service via client version header regex backtracking ([GHSA-38m6-82c8-4xfm](https://github.com/parse-community/parse-server/security/advisories/GHSA-38m6-82c8-4xfm)) ([#10463](https://github.com/parse-community/parse-server/issues/10463)) ([56c159e](https://github.com/parse-community/parse-server/commit/56c159ec962d729df09ccaa5cc2537751511e375))

# [9.9.0-alpha.3](https://github.com/parse-community/parse-server/compare/9.9.0-alpha.2...9.9.0-alpha.3) (2026-04-30)


Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "9.9.0",
"version": "9.9.1-alpha.3",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
Expand Down
49 changes: 0 additions & 49 deletions spec/ClientSDK.spec.js

This file was deleted.

10 changes: 0 additions & 10 deletions spec/Middlewares.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ describe('middlewares', () => {
});

const BodyParams = {
clientVersion: '_ClientVersion',
installationId: '_InstallationId',
sessionToken: '_SessionToken',
masterKey: '_MasterKey',
Expand Down Expand Up @@ -468,12 +467,6 @@ describe('middlewares', () => {
expect(fakeRes.status).toHaveBeenCalledWith(403);
});

it('should reject non-string _ClientVersion in body', async () => {
fakeReq.body._ClientVersion = { toLowerCase: 'evil' };
await middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});

it('should reject non-string _InstallationId in body', async () => {
fakeReq.body._InstallationId = { toString: 'evil' };
await middlewares.handleParseHeaders(fakeReq, fakeRes);
Expand Down Expand Up @@ -502,7 +495,6 @@ describe('middlewares', () => {
// Each request should be handled independently without affecting server stability.
const payloads = [
{ _SessionToken: { toString: 'evil' } },
{ _ClientVersion: { toLowerCase: 'evil' } },
{ _InstallationId: [1, 2, 3] },
{ _ContentType: { toString: 'evil' } },
];
Expand Down Expand Up @@ -539,12 +531,10 @@ describe('middlewares', () => {

it('should still accept valid string body fields', done => {
fakeReq.body._SessionToken = 'r:validtoken';
fakeReq.body._ClientVersion = 'js1.0.0';
fakeReq.body._InstallationId = 'install123';
fakeReq.body._ContentType = 'application/json';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeReq.info.sessionToken).toEqual('r:validtoken');
expect(fakeReq.info.clientVersion).toEqual('js1.0.0');
expect(fakeReq.info.installationId).toEqual('install123');
expect(fakeReq.headers['content-type']).toEqual('application/json');
done();
Expand Down
109 changes: 109 additions & 0 deletions spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,115 @@ describe('ParseGraphQLServer', () => {
expect(introspection.data).toBeDefined();
expect(introspection.data.__type).toBeDefined();
});

it('should strip "Did you mean" field suggestions from validation errors without master or maintenance key', async () => {
try {
await apolloClient.query({
query: gql`
query Typo {
healt
}
`,
});
fail('should have thrown a validation error');
} catch (e) {
const message = e.networkError.result.errors[0].message;
expect(message).toContain('Cannot query field "healt"');
expect(message).not.toMatch(/Did you mean/);
expect(message).not.toContain('health');
}
});

it('should strip "Did you mean" argument suggestions from validation errors without master or maintenance key', async () => {
try {
await apolloClient.query({
query: gql`
query UnknownArg {
users(wher: {}) {
edges {
node {
id
}
}
}
}
`,
});
fail('should have thrown a validation error');
} catch (e) {
const message = e.networkError.result.errors[0].message;
expect(message).toContain('Unknown argument "wher"');
expect(message).not.toMatch(/Did you mean/);
expect(message).not.toContain('"where"');
}
});

it('should keep "Did you mean" suggestions with master key', async () => {
try {
await apolloClient.query({
query: gql`
query Typo {
healt
}
`,
context: {
headers: {
'X-Parse-Master-Key': 'test',
},
},
});
fail('should have thrown a validation error');
} catch (e) {
const message = e.networkError.result.errors[0].message;
expect(message).toContain('Cannot query field "healt"');
expect(message).toMatch(/Did you mean/);
expect(message).toContain('health');
}
});

it('should keep "Did you mean" suggestions with maintenance key', async () => {
try {
await apolloClient.query({
query: gql`
query Typo {
healt
}
`,
context: {
headers: {
'X-Parse-Maintenance-Key': 'test2',
},
},
});
fail('should have thrown a validation error');
} catch (e) {
const message = e.networkError.result.errors[0].message;
expect(message).toContain('Cannot query field "healt"');
expect(message).toMatch(/Did you mean/);
expect(message).toContain('health');
}
});

it('should keep "Did you mean" suggestions when public introspection is enabled', async () => {
const parseServer = await reconfigureServer();
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });

try {
await apolloClient.query({
query: gql`
query Typo {
healt
}
`,
});
fail('should have thrown a validation error');
} catch (e) {
const message = e.networkError.result.errors[0].message;
expect(message).toContain('Cannot query field "healt"');
expect(message).toMatch(/Did you mean/);
expect(message).toContain('health');
}
});
});


Expand Down
Loading
Loading