Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
56c159e
fix: Pre-authentication denial of service via client version header r…
mtrezza May 17, 2026
216cf83
chore(release): 9.9.1-alpha.1 [skip ci]
semantic-release-bot May 17, 2026
155123a
fix: GraphQL "Did you mean" validation suggestions disclose schema to…
mtrezza May 18, 2026
828d0e0
chore(release): 9.9.1-alpha.2 [skip ci]
semantic-release-bot May 18, 2026
0ae0ed3
test: GraphQL endpoint is exempt from routeAllowList by design (#10480)
mtrezza May 26, 2026
552c6dd
fix: Server option routeAllowList is bypassable through batch sub-req…
mtrezza May 27, 2026
1e0d6ce
chore(release): 9.9.1-alpha.3 [skip ci]
semantic-release-bot May 27, 2026
66484ce
fix: Stored XSS via trailing-dot filename bypassing file upload exten…
mtrezza Jun 1, 2026
2b9d93d
chore(release): 9.9.1-alpha.4 [skip ci]
semantic-release-bot Jun 1, 2026
83e90ed
fix: Endpoints `/login` and `/verifyPassword` disclose MFA secrets an…
mtrezza Jun 3, 2026
a4118f6
chore(release): 9.9.1-alpha.5 [skip ci]
semantic-release-bot Jun 3, 2026
43658f1
fix: Relation `$relatedTo` query bypasses `protectedFields` and ownin…
mtrezza Jun 3, 2026
07478de
chore(release): 9.9.1-alpha.6 [skip ci]
semantic-release-bot Jun 3, 2026
78859a9
docs: Clarify that rateLimit applies to REST API routes only and not …
mtrezza Jun 4, 2026
f12e1c3
fix: Cloud Function multipart requests bypass the maxUploadSize limit…
mtrezza Jun 6, 2026
c700ebd
chore(release): 9.9.1-alpha.7 [skip ci]
semantic-release-bot Jun 6, 2026
3fad4fb
fix: LiveQuery subscriptions leak when a client reuses a subscribe re…
mtrezza Jun 10, 2026
ca55aaf
chore(release): 9.9.1-alpha.8 [skip ci]
semantic-release-bot Jun 10, 2026
880e8e6
fix: rateLimit on exact static routes is bypassed by appending a quer…
mtrezza Jun 11, 2026
0644675
chore(release): 9.9.1-alpha.9 [skip ci]
semantic-release-bot Jun 11, 2026
f861210
fix: Middleware route checks do not match routing-equivalent path var…
mtrezza Jun 12, 2026
576f4f6
chore(release): 9.9.1-alpha.10 [skip ci]
semantic-release-bot Jun 12, 2026
30f1612
test: LiveQuery cross-origin connections receive only public-read dat…
mtrezza Jun 13, 2026
be12a60
fix: Stored XSS via non-standard file extension bypassing file upload…
mtrezza Jun 16, 2026
ccf85f9
chore(release): 9.9.1-alpha.11 [skip ci]
semantic-release-bot Jun 16, 2026
1103c7a
fix: Denial of service via exponential-time processing of deeply nest…
mtrezza Jun 17, 2026
3de7aa3
chore(release): 9.9.1-alpha.12 [skip ci]
semantic-release-bot Jun 17, 2026
e9c85df
fix: LiveQuery discloses object data to a subscriber across an ACL re…
mtrezza Jun 19, 2026
6ed35db
chore(release): 9.9.1-alpha.13 [skip ci]
semantic-release-bot Jun 19, 2026
816078f
feat: Add option to disallow aggregation pipelines for the read-only …
mtrezza Jun 19, 2026
37039b0
chore(release): 9.10.0-alpha.1 [skip ci]
semantic-release-bot Jun 19, 2026
4d3465c
test: Rate limit requestMethods is scoped to the configured HTTP meth…
mtrezza Jun 20, 2026
cce91e5
fix: Stored XSS via malformed Content-Type bypassing file upload exte…
mtrezza Jun 25, 2026
7e9d53a
chore(release): 9.10.0-alpha.2 [skip ci]
semantic-release-bot Jun 25, 2026
b4d6c5a
empty commit to trigger CI
github-actions[bot] Jul 1, 2026
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
35 changes: 35 additions & 0 deletions benchmark/performance.js
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,40 @@ async function benchmarkObjectCreateNestedDenylist(name) {
});
}

/**
* Benchmark: $relatedTo relation query (public, non-master)
*
* Measures a public `$relatedTo` query, which now performs an owning-object
* read-access check before reading the relation join table (GHSA-wmwx-jr2p-4j4r).
* This captures the cost of that added authorization read on the relation path.
*/
async function benchmarkRelatedToQuery(name) {
const Child = Parse.Object.extend('BenchmarkRelChild');
const children = [];
for (let i = 0; i < 50; i++) {
children.push(new Child({ value: i }));
}
await Parse.Object.saveAll(children, { useMasterKey: true });

// Publicly readable owning object, so the authorized relation path runs fully.
const Parent = Parse.Object.extend('BenchmarkRelParent');
const parent = new Parent({ name: 'benchmark-parent' });
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
parent.setACL(acl);
parent.relation('members').add(children);
await parent.save(null, { useMasterKey: true });

return measureOperation({
name,
iterations: 1_000,
operation: async () => {
// Non-master query exercises the owning-object read-access check.
await parent.relation('members').query().find();
},
});
}

/**
* Run all benchmarks
*/
Expand Down Expand Up @@ -856,6 +890,7 @@ async function runBenchmarks() {
{ name: 'Object.saveAll (batch save)', fn: benchmarkBatchSave },
{ name: 'Query.get (by objectId)', fn: benchmarkObjectRead },
{ name: 'Query.find (simple query)', fn: benchmarkSimpleQuery },
{ name: 'Query.find ($relatedTo relation)', fn: benchmarkRelatedToQuery },
{ name: 'User.signUp', fn: benchmarkUserSignup },
{ name: 'User.login', fn: benchmarkUserLogin },
{ name: 'Query.include (parallel pointers)', fn: benchmarkQueryWithIncludeParallel },
Expand Down
105 changes: 105 additions & 0 deletions changelogs/CHANGELOG_alpha.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,108 @@
# [9.10.0-alpha.2](https://github.com/parse-community/parse-server/compare/9.10.0-alpha.1...9.10.0-alpha.2) (2026-06-25)


### Bug Fixes

* Stored XSS via malformed Content-Type bypassing file upload extension blocklist ([GHSA-r899-h629-j84r](https://github.com/parse-community/parse-server/security/advisories/GHSA-r899-h629-j84r)) ([#10521](https://github.com/parse-community/parse-server/issues/10521)) ([cce91e5](https://github.com/parse-community/parse-server/commit/cce91e554818492d1b153c46dc3b91fa6e0309bc))

# [9.10.0-alpha.1](https://github.com/parse-community/parse-server/compare/9.9.1-alpha.13...9.10.0-alpha.1) (2026-06-19)


### Features

* Add option to disallow aggregation pipelines for the read-only master key ([#10517](https://github.com/parse-community/parse-server/issues/10517)) ([816078f](https://github.com/parse-community/parse-server/commit/816078fff7f95f333a99c1e2d7166a585742d466))

## [9.9.1-alpha.13](https://github.com/parse-community/parse-server/compare/9.9.1-alpha.12...9.9.1-alpha.13) (2026-06-19)


### Bug Fixes

* LiveQuery discloses object data to a subscriber across an ACL read-access change ([GHSA-97pr-9hgg-3p8r](https://github.com/parse-community/parse-server/security/advisories/GHSA-97pr-9hgg-3p8r)) ([#10515](https://github.com/parse-community/parse-server/issues/10515)) ([e9c85df](https://github.com/parse-community/parse-server/commit/e9c85dfe40a866a55ebae3b6ae56285ac0a22e64))

## [9.9.1-alpha.12](https://github.com/parse-community/parse-server/compare/9.9.1-alpha.11...9.9.1-alpha.12) (2026-06-17)


### Bug Fixes

* Denial of service via exponential-time processing of deeply nested query operators ([GHSA-cgxm-vr2f-6fj8](https://github.com/parse-community/parse-server/security/advisories/GHSA-cgxm-vr2f-6fj8)) ([#10511](https://github.com/parse-community/parse-server/issues/10511)) ([1103c7a](https://github.com/parse-community/parse-server/commit/1103c7a890e0455ba3dccd4bc5db17efe1789c9a))

## [9.9.1-alpha.11](https://github.com/parse-community/parse-server/compare/9.9.1-alpha.10...9.9.1-alpha.11) (2026-06-16)


### Bug Fixes

* Stored XSS via non-standard file extension bypassing file upload extension blocklist ([GHSA-v8x7-r927-cc93](https://github.com/parse-community/parse-server/security/advisories/GHSA-v8x7-r927-cc93)) ([#10505](https://github.com/parse-community/parse-server/issues/10505)) ([be12a60](https://github.com/parse-community/parse-server/commit/be12a60d65b6e140481882037fb896b1f951df50))

## [9.9.1-alpha.10](https://github.com/parse-community/parse-server/compare/9.9.1-alpha.9...9.9.1-alpha.10) (2026-06-12)


### Bug Fixes

* Middleware route checks do not match routing-equivalent path variants (trailing slash, case) ([#10501](https://github.com/parse-community/parse-server/issues/10501)) ([f861210](https://github.com/parse-community/parse-server/commit/f8612109e3175399b4f814efcc961128de6143a5))

## [9.9.1-alpha.9](https://github.com/parse-community/parse-server/compare/9.9.1-alpha.8...9.9.1-alpha.9) (2026-06-11)


### Bug Fixes

* rateLimit on exact static routes is bypassed by appending a query string ([#10500](https://github.com/parse-community/parse-server/issues/10500)) ([880e8e6](https://github.com/parse-community/parse-server/commit/880e8e6929fd62ed3680b138613bcfdcd572db07))

## [9.9.1-alpha.8](https://github.com/parse-community/parse-server/compare/9.9.1-alpha.7...9.9.1-alpha.8) (2026-06-10)


### Bug Fixes

* LiveQuery subscriptions leak when a client reuses a subscribe requestId ([#10499](https://github.com/parse-community/parse-server/issues/10499)) ([3fad4fb](https://github.com/parse-community/parse-server/commit/3fad4fb1c4b41f51dab96532245bb302d2be30e6))

## [9.9.1-alpha.7](https://github.com/parse-community/parse-server/compare/9.9.1-alpha.6...9.9.1-alpha.7) (2026-06-06)


### Bug Fixes

* Cloud Function multipart requests bypass the maxUploadSize limit ([#10498](https://github.com/parse-community/parse-server/issues/10498)) ([f12e1c3](https://github.com/parse-community/parse-server/commit/f12e1c3e31fb211bb7fe106a9a295ac7d0dd4ea7))

## [9.9.1-alpha.6](https://github.com/parse-community/parse-server/compare/9.9.1-alpha.5...9.9.1-alpha.6) (2026-06-03)


### Bug Fixes

* Relation `$relatedTo` query bypasses `protectedFields` and owning-object ACL ([GHSA-wmwx-jr2p-4j4r](https://github.com/parse-community/parse-server/security/advisories/GHSA-wmwx-jr2p-4j4r)) ([#10493](https://github.com/parse-community/parse-server/issues/10493)) ([43658f1](https://github.com/parse-community/parse-server/commit/43658f1fd83689b24a4350094f1071ac555ac9b3))

## [9.9.1-alpha.5](https://github.com/parse-community/parse-server/compare/9.9.1-alpha.4...9.9.1-alpha.5) (2026-06-03)


### Bug Fixes

* Endpoints `/login` and `/verifyPassword` disclose MFA secrets and protected fields when `_User` get is denied ([GHSA-75v4-m273-5j49](https://github.com/parse-community/parse-server/security/advisories/GHSA-75v4-m273-5j49)) ([#10492](https://github.com/parse-community/parse-server/issues/10492)) ([83e90ed](https://github.com/parse-community/parse-server/commit/83e90edbe4224c81172a20e40fa986662c9394ca))

## [9.9.1-alpha.4](https://github.com/parse-community/parse-server/compare/9.9.1-alpha.3...9.9.1-alpha.4) (2026-06-01)


### Bug Fixes

* Stored XSS via trailing-dot filename bypassing file upload extension blocklist ([GHSA-7wqv-xjf3-x35v](https://github.com/parse-community/parse-server/security/advisories/GHSA-7wqv-xjf3-x35v)) ([#10489](https://github.com/parse-community/parse-server/issues/10489)) ([66484ce](https://github.com/parse-community/parse-server/commit/66484ce8fdd87a5d4c23bf9e40f7ea379b4dce79))

## [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.10.0-alpha.2",
"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.

58 changes: 58 additions & 0 deletions spec/CloudCodeMultipart.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,4 +363,62 @@ describe('Cloud Code Multipart', () => {
expect(result.status).toBe(200);
expect(result.data.result.isMaster).toBe(false);
});

it('should reject multipart request with many empty parts whose wire size exceeds maxUploadSize', async () => {
await reconfigureServer({ maxUploadSize: '1kb' });

Parse.Cloud.define('multipartManyEmptyParts', req => {
return { count: Object.keys(req.params).length };
});

const boundary = '----TestBoundaryManyEmptyParts';
const parts = [];
for (let i = 0; i < 2000; i++) {
parts.push({ name: `f${i}`, value: '' });
}
const body = buildMultipartBody(boundary, parts);
// The wire body is far larger than maxUploadSize even though every field
// value is empty, so the value/chunk byte counters alone never trip.
expect(body.length).toBeGreaterThan(100 * 1024);

const result = await postMultipart(
`http://localhost:8378/1/functions/multipartManyEmptyParts`,
{
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
body
);

expect(result.data.code).toBe(Parse.Error.OBJECT_TOO_LARGE);
});

it('should reject multipart request whose Content-Length exceeds maxUploadSize', async () => {
await reconfigureServer({ maxUploadSize: '1kb' });

Parse.Cloud.define('multipartContentLength', req => {
return { count: Object.keys(req.params).length };
});

const boundary = '----TestBoundaryContentLength';
const parts = [];
for (let i = 0; i < 2000; i++) {
parts.push({ name: `f${i}`, value: '' });
}
const body = buildMultipartBody(boundary, parts);

const result = await postMultipart(
`http://localhost:8378/1/functions/multipartContentLength`,
{
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': String(body.length),
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
body
);

expect(result.data.code).toBe(Parse.Error.OBJECT_TOO_LARGE);
});
});
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
Loading
Loading