Compare commits

...
Author SHA1 Message Date
GitHub Actions 24b53c0679 empty commit to trigger CI 2026-08-01 00:31:43 +00:00
semantic-release-bot 315e157637 chore(release): 9.10.1-alpha.6 [skip ci]
## [9.10.1-alpha.6](https://github.com/parse-community/parse-server/compare/9.10.1-alpha.5...9.10.1-alpha.6) (2026-07-26)

### Bug Fixes

* `Parse.Query.explain` runs afterFind trigger on query plan results ([#10536](https://github.com/parse-community/parse-server/issues/10536)) ([64d58ff](https://github.com/parse-community/parse-server/commit/64d58ff726b42b45e107e27bc1f3b50be82b3189))
2026-07-26 00:55:49 +00:00
Daniel 64d58ff726 fix: Parse.Query.explain runs afterFind trigger on query plan results (#10536) 2026-07-26 02:55:02 +02:00
Manuel 9e4e5dbbc9 ci: Skip CI checks for draft pull requests (#10610) 2026-07-25 15:42:05 +02:00
semantic-release-bot 472136c5ca chore(release): 9.10.1-alpha.5 [skip ci]
## [9.10.1-alpha.5](https://github.com/parse-community/parse-server/compare/9.10.1-alpha.4...9.10.1-alpha.5) (2026-07-25)

### Bug Fixes

* Server crash from unhandled promise rejection when multiple Cloud Code validator fields fail ([#10540](https://github.com/parse-community/parse-server/issues/10540)) ([90c2778](https://github.com/parse-community/parse-server/commit/90c277894f31be8e64a4b049ad503621cf5f2285))
2026-07-25 01:40:12 +00:00
Daniel 90c277894f fix: Server crash from unhandled promise rejection when multiple Cloud Code validator fields fail (#10540) 2026-07-25 03:39:21 +02:00
semantic-release-bot 99a0471aaf chore(release): 9.10.1-alpha.4 [skip ci]
## [9.10.1-alpha.4](https://github.com/parse-community/parse-server/compare/9.10.1-alpha.3...9.10.1-alpha.4) (2026-07-24)

### Bug Fixes

* Install the latest Parse Server version in bootstrap.sh ([#10556](https://github.com/parse-community/parse-server/issues/10556)) ([997ee15](https://github.com/parse-community/parse-server/commit/997ee152c358524bd2b0dded490255742b61a1ba))
2026-07-24 14:24:35 +00:00
Daniel 997ee152c3 fix: Install the latest Parse Server version in bootstrap.sh (#10556) 2026-07-24 16:23:44 +02:00
Daniel 81bdeb8697 refactor: Correct under-reported code coverage for Options/parsers (#10559) 2026-07-24 15:50:39 +02:00
Daniel 5838c07acc ci: Exclude build tooling (resources/) from coverage (#10561) 2026-07-21 01:47:03 +02:00
Daniel 6f3e07ca80 docs: Clarify LiveQuery options in --help output (#10558) 2026-07-21 01:26:37 +02:00
Manuel b9912b0bb5 refactor: Bump @actions/core from 3.0.0 to 3.0.1 (#10593) 2026-07-21 00:45:18 +02:00
Manuel 154e1d48bd refactor: Bump mongodb-runner from 5.9.3 to 6.8.3 (#10591) 2026-07-21 00:19:12 +02:00
Manuel 34c8f759c9 refactor: Bump @babel/preset-env from 7.29.2 to 7.29.7 (#10592) 2026-07-20 23:49:54 +02:00
dependabot[bot] b45f7ea3bf refactor: Bump websocket-driver from 0.7.4 to 0.7.5 (#10583) 2026-07-20 14:05:03 +02:00
Manuel 71e65572cb refactor: Bump @babel/core from 7.29.6 to 7.29.7 (#10588) 2026-07-20 02:52:22 +02:00
Manuel 0ebd93569e refactor: Bump @semantic-release/release-notes-generator from 14.1.0 to 14.1.1 (#10587) 2026-07-19 16:54:16 +02:00
semantic-release-bot c6fd388e27 chore(release): 9.10.1-alpha.3 [skip ci]
## [9.10.1-alpha.3](https://github.com/parse-community/parse-server/compare/9.10.1-alpha.2...9.10.1-alpha.3) (2026-07-16)

### Bug Fixes

* Bump ws from 8.20.0 to 8.21.0 ([#10576](https://github.com/parse-community/parse-server/issues/10576)) ([629426f](https://github.com/parse-community/parse-server/commit/629426f00d619ca624fc2d7e64d98f406ce20acc))
2026-07-16 20:18:30 +00:00
Manuel 629426f00d fix: Bump ws from 8.20.0 to 8.21.0 (#10576) 2026-07-16 22:17:21 +02:00
Manuel eab2e97482 refactor: Bump cross-env from 7.0.3 to 10.1.0 (#10579) 2026-07-16 22:01:27 +02:00
semantic-release-bot 42ae75f001 chore(release): 9.10.1-alpha.2 [skip ci]
## [9.10.1-alpha.2](https://github.com/parse-community/parse-server/compare/9.10.1-alpha.1...9.10.1-alpha.2) (2026-07-14)

### Bug Fixes

* Creating a session can delete another user's session ([#10582](https://github.com/parse-community/parse-server/issues/10582)) ([0df8779](https://github.com/parse-community/parse-server/commit/0df8779c2ecccb055e615d42ee85ae2bce491670))
2026-07-14 16:31:56 +00:00
Manuel 0df8779c2e fix: Creating a session can delete another user's session (#10582) 2026-07-14 18:31:04 +02:00
Manuel d76845f058 test: Add _Installation non-master access control regression tests (#10578) 2026-07-14 15:12:14 +02:00
semantic-release-bot 6e87eb2544 chore(release): 9.10.1-alpha.1 [skip ci]
## [9.10.1-alpha.1](https://github.com/parse-community/parse-server/compare/9.10.0...9.10.1-alpha.1) (2026-07-13)

### Bug Fixes

* Bump follow-redirects from 1.15.11 to 1.16.0 ([#10577](https://github.com/parse-community/parse-server/issues/10577)) ([d577327](https://github.com/parse-community/parse-server/commit/d577327bff073b0436a158e9694feed8eed2f8e7))
2026-07-13 22:58:17 +00:00
Manuel d577327bff fix: Bump follow-redirects from 1.15.11 to 1.16.0 (#10577) 2026-07-14 00:57:28 +02:00
dependabot[bot] 7d4d135d3f refactor: Bump fast-xml-builder from 1.1.4 to 1.2.0 (#10457) 2026-07-13 14:12:01 +02:00
dependabot[bot] a7e792ac19 refactor: Bump @babel/plugin-transform-modules-systemjs from 7.29.0 to 7.29.7 (#10458) 2026-07-13 14:11:58 +02:00
dependabot[bot] a2ea125678 refactor: Bump fast-xml-parser from 5.5.9 to 5.8.0 (#10447) 2026-07-13 14:05:18 +02:00
dependabot[bot] 538b1d7088 refactor: Bump postcss from 8.4.47 to 8.5.14 (#10450) 2026-07-13 14:05:14 +02:00
dependabot[bot] 1b487e4c3f refactor: Bump @protobufjs/utf8 from 1.1.0 to 1.1.1 (#10461) 2026-07-13 14:05:10 +02:00
dependabot[bot] 3f4d0d7c3c refactor: Bump qs from 6.14.2 to 6.15.2 (#10476) 2026-07-13 14:05:06 +02:00
dependabot[bot] a972046b2e refactor: Bump @grpc/grpc-js from 1.14.3 to 1.14.4 (#10503) 2026-07-13 14:05:03 +02:00
Manuel b28dba8812 refactor: Bump typescript-eslint from 8.58.0 to 8.59.1 (#10575) 2026-07-13 13:49:35 +02:00
Manuel 0d260ff1ed refactor: Bump @babel/core from 7.29.0 to 7.29.6 (#10574) 2026-07-13 12:38:10 +02:00
dependabot[bot] 504f919415 refactor: Bump form-data (#10508) 2026-07-13 12:05:07 +02:00
dependabot[bot] 5c25152740 refactor: Bump markdown-it from 14.1.0 to 14.3.0 (#10510) 2026-07-13 05:22:04 +02:00
dependabot[bot] 2275feed3e refactor: Bump undici from 6.24.1 to 6.27.0 (#10524) 2026-07-13 04:31:19 +02:00
semantic-release-bot b5ca12fa5e chore(release): 9.10.0 [skip ci]
# [9.10.0](https://github.com/parse-community/parse-server/compare/9.9.0...9.10.0) (2026-07-13)

### Bug Fixes

* Cloud Code beforeFind trigger context is not isolated from prototype pollution ([#10570](https://github.com/parse-community/parse-server/issues/10570)) ([bea001e](https://github.com/parse-community/parse-server/commit/bea001e7ef9cf991e99fe1eb1e8e25ef71c530f7))
* 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))
* 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))
* 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))
* GeoPoint distance queries fail with an internal server error on MongoDB 8.3 and later ([#10572](https://github.com/parse-community/parse-server/issues/10572)) ([b706c22](https://github.com/parse-community/parse-server/commit/b706c22cd9e47d91ec00d3f6050d161fd721e2e1))
* 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))
* GraphQL error messages disclose pointer and relation target class names when public introspection is disabled ([GHSA-r2g6-4f6j-f6rf](https://github.com/parse-community/parse-server/security/advisories/GHSA-r2g6-4f6j-f6rf)) ([#10568](https://github.com/parse-community/parse-server/issues/10568)) ([cb9b542](https://github.com/parse-community/parse-server/commit/cb9b54264d4dae4c0f9306924c34d805130b600e))
* GraphQL error messages disclose required input field names when public introspection is disabled ([GHSA-2fgh-8j2g-w354](https://github.com/parse-community/parse-server/security/advisories/GHSA-2fgh-8j2g-w354)) ([#10566](https://github.com/parse-community/parse-server/issues/10566)) ([d96c945](https://github.com/parse-community/parse-server/commit/d96c945b6ddee652ecf27b240292e569367be550)), closes [GHSA-2f#8j2g-w354](https://github.com/GHSA-2f/issues/8j2g-w354) [/github.com/parse-community/parse-server/security/advisories/GHSA-2f#8j2g-w354](https://github.com//github.com/parse-community/parse-server/security/advisories/GHSA-2f/issues/8j2g-w354)
* GraphQL variable-coercion suggestions disclose schema to unauthenticated callers ([GHSA-9g8f-h8f3-hjcm](https://github.com/parse-community/parse-server/security/advisories/GHSA-9g8f-h8f3-hjcm)) ([#10563](https://github.com/parse-community/parse-server/issues/10563)) ([2625489](https://github.com/parse-community/parse-server/commit/2625489a27c07aed1283c5dc9c5e8f8f53a9e44c))
* 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))
* 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))
* 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))
* NumberOrBoolean config option (cluster) value not coerced from env/CLI ([#10531](https://github.com/parse-community/parse-server/issues/10531)) ([459786f](https://github.com/parse-community/parse-server/commit/459786fd41cc835ec650d951258f011737cdb4c9))
* 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))
* 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))
* 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))
* 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))
* 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))
* 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))
* 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))

### 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))
2026-07-13 01:05:22 +00:00
Manuel fb1d6fc186 build: Release (#10573) 2026-07-13 03:04:18 +02:00
GitHub Actions 534a6b92d0 empty commit to trigger CI 2026-07-13 00:44:39 +00:00
semantic-release-bot 0686dc0b3b chore(release): 9.10.0-alpha.8 [skip ci]
# [9.10.0-alpha.8](https://github.com/parse-community/parse-server/compare/9.10.0-alpha.7...9.10.0-alpha.8) (2026-07-13)

### Bug Fixes

* GeoPoint distance queries fail with an internal server error on MongoDB 8.3 and later ([#10572](https://github.com/parse-community/parse-server/issues/10572)) ([b706c22](https://github.com/parse-community/parse-server/commit/b706c22cd9e47d91ec00d3f6050d161fd721e2e1))
2026-07-13 00:39:59 +00:00
Manuel b706c22cd9 fix: GeoPoint distance queries fail with an internal server error on MongoDB 8.3 and later (#10572) 2026-07-13 02:39:05 +02:00
semantic-release-bot 1bdb6a411f chore(release): 9.10.0-alpha.7 [skip ci]
# [9.10.0-alpha.7](https://github.com/parse-community/parse-server/compare/9.10.0-alpha.6...9.10.0-alpha.7) (2026-07-11)

### Bug Fixes

* Cloud Code beforeFind trigger context is not isolated from prototype pollution ([#10570](https://github.com/parse-community/parse-server/issues/10570)) ([bea001e](https://github.com/parse-community/parse-server/commit/bea001e7ef9cf991e99fe1eb1e8e25ef71c530f7))
2026-07-11 01:46:38 +00:00
Manuel bea001e7ef fix: Cloud Code beforeFind trigger context is not isolated from prototype pollution (#10570) 2026-07-11 03:45:38 +02:00
semantic-release-bot 348d90ed54 chore(release): 9.10.0-alpha.6 [skip ci]
# [9.10.0-alpha.6](https://github.com/parse-community/parse-server/compare/9.10.0-alpha.5...9.10.0-alpha.6) (2026-07-10)

### Bug Fixes

* GraphQL error messages disclose pointer and relation target class names when public introspection is disabled ([GHSA-r2g6-4f6j-f6rf](https://github.com/parse-community/parse-server/security/advisories/GHSA-r2g6-4f6j-f6rf)) ([#10568](https://github.com/parse-community/parse-server/issues/10568)) ([cb9b542](https://github.com/parse-community/parse-server/commit/cb9b54264d4dae4c0f9306924c34d805130b600e))
2026-07-10 22:29:26 +00:00
Manuel cb9b54264d fix: GraphQL error messages disclose pointer and relation target class names when public introspection is disabled ([GHSA-r2g6-4f6j-f6rf](https://github.com/parse-community/parse-server/security/advisories/GHSA-r2g6-4f6j-f6rf)) (#10568) 2026-07-11 00:28:29 +02:00
23 changed files with 3625 additions and 2021 deletions
+16
View File
@@ -3,6 +3,7 @@ on:
push:
branches: [release, alpha, beta, next-major, 'release-[0-9]+.x.x']
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches:
- '**'
paths-ignore:
@@ -15,6 +16,7 @@ permissions:
jobs:
check-code-analysis:
name: Code Analysis
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
permissions:
actions: read
@@ -36,6 +38,7 @@ jobs:
uses: github/codeql-action/analyze@v2
check-ci:
name: Node Engine Check
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
@@ -52,6 +55,7 @@ jobs:
run: npm run ci:checkNodeEngine
check-lint:
name: Lint
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
@@ -72,6 +76,7 @@ jobs:
- run: npm run lint
check-definitions:
name: Check Definitions
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
@@ -93,6 +98,7 @@ jobs:
run: npm run ci:definitionsCheck
check-circular:
name: Circular Dependencies
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
@@ -113,6 +119,7 @@ jobs:
- run: npm run madge:circular
check-docs:
name: Docs
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
@@ -136,6 +143,7 @@ jobs:
run: npm run docs
check-docker:
name: Docker Build
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
@@ -153,6 +161,7 @@ jobs:
platforms: linux/amd64, linux/arm64/v8
check-lock-file-version:
name: NPM Lock File Version
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
@@ -166,6 +175,7 @@ jobs:
fi
check-types:
name: Check Types
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
@@ -187,6 +197,10 @@ jobs:
MONGODB_VERSION: 8.0.4
MONGODB_TOPOLOGY: replset
NODE_VERSION: 24.11.0
- name: MongoDB 8.3, ReplicaSet
MONGODB_VERSION: 8.3.4
MONGODB_TOPOLOGY: replset
NODE_VERSION: 24.11.0
- name: Redis Cache
PARSE_SERVER_TEST_CACHE: redis
MONGODB_VERSION: 8.0.4
@@ -202,6 +216,7 @@ jobs:
NODE_VERSION: 22.12.0
fail-fast: false
name: ${{ matrix.name }}
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
timeout-minutes: 20
runs-on: ubuntu-latest
services:
@@ -256,6 +271,7 @@ jobs:
NODE_VERSION: 24.11.0
fail-fast: false
name: ${{ matrix.name }}
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
timeout-minutes: 20
runs-on: ubuntu-latest
services:
+2 -1
View File
@@ -4,7 +4,8 @@
"text-summary"
],
"exclude": [
"**/spec/**"
"**/spec/**",
"resources/**"
]
}
+2 -4
View File
@@ -162,9 +162,7 @@ cat > ./package.json << EOF
"scripts": {
"start": "parse-server config.json"
},
"dependencies": {
"parse-server": "^3.9.0"
}
"dependencies": {}
}
EOF
echo "${CHECK} Created package.json"
@@ -195,7 +193,7 @@ fi
echo "\n${CHECK} running npm install\n"
npm install -s
npm install parse-server -s
CURL_CMD=$(cat << EOF
curl -X POST -H 'X-Parse-Application-Id: ${APP_ID}' \\
+63
View File
@@ -1,3 +1,66 @@
## [9.10.1-alpha.6](https://github.com/parse-community/parse-server/compare/9.10.1-alpha.5...9.10.1-alpha.6) (2026-07-26)
### Bug Fixes
* `Parse.Query.explain` runs afterFind trigger on query plan results ([#10536](https://github.com/parse-community/parse-server/issues/10536)) ([64d58ff](https://github.com/parse-community/parse-server/commit/64d58ff726b42b45e107e27bc1f3b50be82b3189))
## [9.10.1-alpha.5](https://github.com/parse-community/parse-server/compare/9.10.1-alpha.4...9.10.1-alpha.5) (2026-07-25)
### Bug Fixes
* Server crash from unhandled promise rejection when multiple Cloud Code validator fields fail ([#10540](https://github.com/parse-community/parse-server/issues/10540)) ([90c2778](https://github.com/parse-community/parse-server/commit/90c277894f31be8e64a4b049ad503621cf5f2285))
## [9.10.1-alpha.4](https://github.com/parse-community/parse-server/compare/9.10.1-alpha.3...9.10.1-alpha.4) (2026-07-24)
### Bug Fixes
* Install the latest Parse Server version in bootstrap.sh ([#10556](https://github.com/parse-community/parse-server/issues/10556)) ([997ee15](https://github.com/parse-community/parse-server/commit/997ee152c358524bd2b0dded490255742b61a1ba))
## [9.10.1-alpha.3](https://github.com/parse-community/parse-server/compare/9.10.1-alpha.2...9.10.1-alpha.3) (2026-07-16)
### Bug Fixes
* Bump ws from 8.20.0 to 8.21.0 ([#10576](https://github.com/parse-community/parse-server/issues/10576)) ([629426f](https://github.com/parse-community/parse-server/commit/629426f00d619ca624fc2d7e64d98f406ce20acc))
## [9.10.1-alpha.2](https://github.com/parse-community/parse-server/compare/9.10.1-alpha.1...9.10.1-alpha.2) (2026-07-14)
### Bug Fixes
* Creating a session can delete another user's session ([#10582](https://github.com/parse-community/parse-server/issues/10582)) ([0df8779](https://github.com/parse-community/parse-server/commit/0df8779c2ecccb055e615d42ee85ae2bce491670))
## [9.10.1-alpha.1](https://github.com/parse-community/parse-server/compare/9.10.0...9.10.1-alpha.1) (2026-07-13)
### Bug Fixes
* Bump follow-redirects from 1.15.11 to 1.16.0 ([#10577](https://github.com/parse-community/parse-server/issues/10577)) ([d577327](https://github.com/parse-community/parse-server/commit/d577327bff073b0436a158e9694feed8eed2f8e7))
# [9.10.0-alpha.8](https://github.com/parse-community/parse-server/compare/9.10.0-alpha.7...9.10.0-alpha.8) (2026-07-13)
### Bug Fixes
* GeoPoint distance queries fail with an internal server error on MongoDB 8.3 and later ([#10572](https://github.com/parse-community/parse-server/issues/10572)) ([b706c22](https://github.com/parse-community/parse-server/commit/b706c22cd9e47d91ec00d3f6050d161fd721e2e1))
# [9.10.0-alpha.7](https://github.com/parse-community/parse-server/compare/9.10.0-alpha.6...9.10.0-alpha.7) (2026-07-11)
### Bug Fixes
* Cloud Code beforeFind trigger context is not isolated from prototype pollution ([#10570](https://github.com/parse-community/parse-server/issues/10570)) ([bea001e](https://github.com/parse-community/parse-server/commit/bea001e7ef9cf991e99fe1eb1e8e25ef71c530f7))
# [9.10.0-alpha.6](https://github.com/parse-community/parse-server/compare/9.10.0-alpha.5...9.10.0-alpha.6) (2026-07-10)
### Bug Fixes
* GraphQL error messages disclose pointer and relation target class names when public introspection is disabled ([GHSA-r2g6-4f6j-f6rf](https://github.com/parse-community/parse-server/security/advisories/GHSA-r2g6-4f6j-f6rf)) ([#10568](https://github.com/parse-community/parse-server/issues/10568)) ([cb9b542](https://github.com/parse-community/parse-server/commit/cb9b54264d4dae4c0f9306924c34d805130b600e))
# [9.10.0-alpha.5](https://github.com/parse-community/parse-server/compare/9.10.0-alpha.4...9.10.0-alpha.5) (2026-07-10)
+30
View File
@@ -1,3 +1,33 @@
# [9.10.0](https://github.com/parse-community/parse-server/compare/9.9.0...9.10.0) (2026-07-13)
### Bug Fixes
* Cloud Code beforeFind trigger context is not isolated from prototype pollution ([#10570](https://github.com/parse-community/parse-server/issues/10570)) ([bea001e](https://github.com/parse-community/parse-server/commit/bea001e7ef9cf991e99fe1eb1e8e25ef71c530f7))
* 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))
* 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))
* 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))
* GeoPoint distance queries fail with an internal server error on MongoDB 8.3 and later ([#10572](https://github.com/parse-community/parse-server/issues/10572)) ([b706c22](https://github.com/parse-community/parse-server/commit/b706c22cd9e47d91ec00d3f6050d161fd721e2e1))
* 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))
* GraphQL error messages disclose pointer and relation target class names when public introspection is disabled ([GHSA-r2g6-4f6j-f6rf](https://github.com/parse-community/parse-server/security/advisories/GHSA-r2g6-4f6j-f6rf)) ([#10568](https://github.com/parse-community/parse-server/issues/10568)) ([cb9b542](https://github.com/parse-community/parse-server/commit/cb9b54264d4dae4c0f9306924c34d805130b600e))
* GraphQL error messages disclose required input field names when public introspection is disabled ([GHSA-2fgh-8j2g-w354](https://github.com/parse-community/parse-server/security/advisories/GHSA-2fgh-8j2g-w354)) ([#10566](https://github.com/parse-community/parse-server/issues/10566)) ([d96c945](https://github.com/parse-community/parse-server/commit/d96c945b6ddee652ecf27b240292e569367be550)), closes [GHSA-2f#8j2g-w354](https://github.com/GHSA-2f/issues/8j2g-w354) [/github.com/parse-community/parse-server/security/advisories/GHSA-2f#8j2g-w354](https://github.com//github.com/parse-community/parse-server/security/advisories/GHSA-2f/issues/8j2g-w354)
* GraphQL variable-coercion suggestions disclose schema to unauthenticated callers ([GHSA-9g8f-h8f3-hjcm](https://github.com/parse-community/parse-server/security/advisories/GHSA-9g8f-h8f3-hjcm)) ([#10563](https://github.com/parse-community/parse-server/issues/10563)) ([2625489](https://github.com/parse-community/parse-server/commit/2625489a27c07aed1283c5dc9c5e8f8f53a9e44c))
* 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))
* 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))
* 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))
* NumberOrBoolean config option (cluster) value not coerced from env/CLI ([#10531](https://github.com/parse-community/parse-server/issues/10531)) ([459786f](https://github.com/parse-community/parse-server/commit/459786fd41cc835ec650d951258f011737cdb4c9))
* 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))
* 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))
* 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))
* 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))
* 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))
* 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))
* 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))
### 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.0](https://github.com/parse-community/parse-server/compare/9.8.0...9.9.0) (2026-05-01)
+2598 -1983
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "9.10.0-alpha.5",
"version": "9.10.1-alpha.6",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -33,7 +33,7 @@
"cors": "2.8.6",
"express": "5.2.1",
"express-rate-limit": "8.3.1",
"follow-redirects": "1.15.11",
"follow-redirects": "1.16.0",
"graphql": "16.13.2",
"graphql-list-fields": "2.0.4",
"graphql-relay": "0.10.2",
@@ -60,17 +60,17 @@
"tv4": "1.3.0",
"winston": "3.19.0",
"winston-daily-rotate-file": "5.0.0",
"ws": "8.20.0"
"ws": "8.21.0"
},
"devDependencies": {
"@actions/core": "3.0.0",
"@actions/core": "3.0.1",
"@apollo/client": "3.13.8",
"@babel/cli": "7.28.6",
"@babel/core": "7.29.0",
"@babel/core": "7.29.7",
"@babel/eslint-parser": "7.28.6",
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
"@babel/plugin-transform-flow-strip-types": "7.27.1",
"@babel/preset-env": "7.29.2",
"@babel/preset-env": "7.29.7",
"@babel/preset-typescript": "7.27.1",
"@saithodev/semantic-release-backmerge": "4.0.1",
"@semantic-release/changelog": "6.0.3",
@@ -78,16 +78,16 @@
"@semantic-release/git": "10.0.1",
"@semantic-release/github": "12.0.6",
"@semantic-release/npm": "13.0.0",
"@semantic-release/release-notes-generator": "14.1.0",
"@semantic-release/release-notes-generator": "14.1.1",
"all-node-versions": "13.0.1",
"apollo-upload-client": "18.0.1",
"clean-jsdoc-theme": "4.3.0",
"cross-env": "7.0.3",
"cross-env": "10.1.0",
"deep-diff": "1.0.2",
"eslint": "9.27.0",
"eslint-plugin-expect-type": "0.6.2",
"eslint-plugin-unused-imports": "4.4.1",
"form-data": "4.0.5",
"form-data": "4.0.6",
"globals": "17.3.0",
"graphql-tag": "2.12.6",
"jasmine": "6.1.0",
@@ -99,14 +99,14 @@
"madge": "8.0.0",
"mock-files-adapter": "file:spec/dependencies/mock-files-adapter",
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
"mongodb-runner": "5.9.3",
"mongodb-runner": "6.8.3",
"node-abort-controller": "3.1.1",
"node-fetch": "3.3.2",
"nyc": "17.1.0",
"prettier": "3.8.1",
"semantic-release": "25.0.3",
"typescript": "5.9.3",
"typescript-eslint": "8.58.0",
"typescript-eslint": "8.59.1",
"yaml": "2.8.3"
},
"scripts": {
+2 -1
View File
@@ -9,7 +9,6 @@
* To rebuild the definitions file, run
* `$ node resources/buildConfigDefinitions.js`
*/
const parsers = require('../src/Options/parsers');
/** The types of nested options. */
const nestedOptionTypes = [
@@ -190,6 +189,8 @@ function mapperFor(elt, t) {
}
function parseDefaultValue(elt, value, t) {
/* istanbul ignore next: lazy require (not module scope) so specs don't double-instrument parsers.js; only reached by `npm run definitions` */
const parsers = require('../src/Options/parsers');
let literalValue;
if (t.isStringTypeAnnotation(elt)) {
if (value == '""' || value == "''") {
+21
View File
@@ -504,6 +504,27 @@ describe('cloud validator', () => {
});
});
it('does not leave an unhandled rejection when multiple fields fail validation (#8826)', async () => {
const rejections = [];
const onUnhandledRejection = reason => rejections.push(reason);
process.on('unhandledRejection', onUnhandledRejection);
try {
Parse.Cloud.define('hello', () => 'Hello world!', {
fields: {
type: { type: String, options: ['Option A', 'Option B'] },
project: { required: true },
},
});
await expectAsync(Parse.Cloud.run('hello', { type: 'Invalid' })).toBeRejectedWith(
jasmine.objectContaining({ code: Parse.Error.VALIDATION_ERROR })
);
await new Promise(resolve => setTimeout(resolve, 100));
expect(rejections).toEqual([]);
} finally {
process.removeListener('unhandledRejection', onUnhandledRejection);
}
});
it('set params options function', done => {
Parse.Cloud.define(
'hello',
+101
View File
@@ -0,0 +1,101 @@
'use strict';
const { MongoClient } = require('mongodb');
const MongoCollection = require('../lib/Adapters/Storage/Mongo/MongoCollection').default;
const { findGeoIndexField } = require('../lib/Adapters/Storage/Mongo/MongoCollection');
describe_only_db('mongo')('MongoCollection', () => {
describe('findGeoIndexField', () => {
it('extracts the field constrained by $nearSphere', () => {
const query = { construct: 'line', location: { $nearSphere: [-121.5, 38.5], $maxDistance: 2.5 } };
expect(findGeoIndexField(query)).toBe('location');
});
it('extracts the field constrained by $near', () => {
expect(findGeoIndexField({ region: { $near: [0, 0] } })).toBe('region');
});
it('recurses into $and to find the geo field', () => {
const query = { $and: [{ a: 1 }, { loc: { $nearSphere: [0, 0] } }] };
expect(findGeoIndexField(query)).toBe('loc');
});
it('returns undefined when there is no geo operator', () => {
expect(findGeoIndexField({ a: 1, b: { $gt: 2 } })).toBeUndefined();
});
it('returns undefined for empty / non-object queries', () => {
expect(findGeoIndexField({})).toBeUndefined();
expect(findGeoIndexField(null)).toBeUndefined();
expect(findGeoIndexField(undefined)).toBeUndefined();
});
it('does not treat $geoWithin as requiring an index', () => {
const query = { location: { $geoWithin: { $centerSphere: [[0, 0], 1] } } };
expect(findGeoIndexField(query)).toBeUndefined();
});
it('does not recurse into $or (MongoDB forbids $near inside $or)', () => {
const query = { $or: [{ a: 1 }, { loc: { $nearSphere: [0, 0] } }] };
expect(findGeoIndexField(query)).toBeUndefined();
});
});
describe('lazy geo index creation', () => {
const collectionName = 'MongoCollectionLazyGeoIndexTest';
let client;
let rawCollection;
const geoQuery = { location: { $nearSphere: [-121.5, 38.5], $maxDistance: 2.526 } };
beforeEach(async () => {
client = new MongoClient(databaseURI);
await client.connect();
rawCollection = client.db().collection(collectionName);
// Start from a clean collection with NO geo index so the lazy-creation path is exercised.
await rawCollection.drop().catch(() => {});
await rawCollection.insertMany([
{ _id: '1', location: [-121, 38] },
{ _id: '2', location: [-122, 39] },
]);
});
afterEach(async () => {
await rawCollection.drop().catch(() => {});
await client.close();
});
it('creates a 2d index on demand and returns results for a $nearSphere query on an un-indexed field', async () => {
const mongoCollection = new MongoCollection(rawCollection);
const results = await mongoCollection.find(geoQuery);
expect(results.length).toBe(2);
const indexes = await rawCollection.indexes();
const hasGeoIndex = indexes.some(index => index.key && index.key.location === '2d');
expect(hasGeoIndex).toBe(true);
});
it_only_mongodb_version('>=8.3')('MongoDB 8.3+ reports the geoNear "no index" error without the field name', async () => {
let error;
try {
await rawCollection.find(geoQuery).toArray();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toMatch(/unable to find index for .geoNear/);
expect(error.message).not.toMatch(/field=/);
});
it_only_mongodb_version('<8.3')('older MongoDB reports the geoNear "no index" error with the field name', async () => {
let error;
try {
await rawCollection.find(geoQuery).toArray();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toMatch(/unable to find index for .geoNear/);
expect(error.message).toMatch(/field=location/);
});
});
});
+371
View File
@@ -1357,6 +1357,377 @@ describe('ParseGraphQLServer', () => {
expect(JSON.stringify(error)).not.toContain('secretRequiredField');
}
});
// A Pointer/Relation field maps to a generated input type whose name embeds the
// pointer's TARGET class (`<Target>PointerInput`, `<Target>RelationWhereInput`,
// `Create<Target>FieldsInput`). graphql-js interpolates that type name into base
// coercion/validation messages that the "Did you mean" and required-field strips do
// not touch, disclosing the target class name to a caller who only supplied the
// pointer field name (which does not reveal its target). Redact those identifiers
// for callers that are not allowed to introspect.
const setupPointerSchema = async _parseServer => {
const schemaController = await _parseServer.config.databaseController.loadSchema();
await schemaController.addClassIfNotExists('SecretAuthor', {
name: { type: 'String' },
});
await schemaController.addClassIfNotExists('DiagBook', {
writtenBy: { type: 'Pointer', targetClass: 'SecretAuthor' },
});
await resetGraphQLCache();
};
it('should strip pointer target class names from where-clause validation errors without master or maintenance key', async () => {
await setupPointerSchema(parseServer);
try {
await apolloClient.query({
query: gql`
query Leak {
diagBooks(where: { writtenBy: 123 }) {
edges {
node {
id
}
}
}
}
`,
});
fail('should have thrown a validation error');
} catch (e) {
const error = getReturnedError(e);
// Expected value of type "SecretAuthorRelationWhereInput", found 123.
expect(error.message).not.toContain('SecretAuthor');
expect(JSON.stringify(error)).not.toContain('SecretAuthor');
}
});
it('should strip pointer target class names from non-object variable-coercion errors without master or maintenance key', async () => {
await setupPointerSchema(parseServer);
try {
await apolloClient.mutate({
mutation: gql`
mutation Create($input: CreateDiagBookInput!) {
createDiagBook(input: $input) {
diagBook {
id
}
}
}
`,
variables: { input: { fields: { writtenBy: { createAndLink: 5 } } } },
});
fail('should have thrown a coercion error');
} catch (e) {
const error = getReturnedError(e);
// Expected type "CreateSecretAuthorFieldsInput" to be an object.
expect(error.message).not.toContain('SecretAuthor');
expect(JSON.stringify(error)).not.toContain('SecretAuthor');
}
});
it('should strip pointer target class names from unknown-field variable-coercion errors without master or maintenance key', async () => {
await setupPointerSchema(parseServer);
try {
await apolloClient.mutate({
mutation: gql`
mutation Create($input: CreateDiagBookInput!) {
createDiagBook(input: $input) {
diagBook {
id
}
}
}
`,
variables: { input: { fields: { writtenBy: { bogusKey: 1 } } } },
});
fail('should have thrown a coercion error');
} catch (e) {
const error = getReturnedError(e);
// Field "bogusKey" is not defined by type "SecretAuthorPointerInput".
expect(error.message).not.toContain('SecretAuthor');
expect(JSON.stringify(error)).not.toContain('SecretAuthor');
}
});
it('should keep pointer target class names in errors with master key', async () => {
await setupPointerSchema(parseServer);
try {
await apolloClient.query({
query: gql`
query Leak {
diagBooks(where: { writtenBy: 123 }) {
edges {
node {
id
}
}
}
}
`,
context: {
headers: {
'X-Parse-Master-Key': 'test',
},
},
});
fail('should have thrown a validation error');
} catch (e) {
const error = getReturnedError(e);
expect(error.message).toContain('SecretAuthor');
}
});
it('should keep pointer target class names in errors when public introspection is enabled', async () => {
const parseServer = await reconfigureServer();
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });
await setupPointerSchema(parseServer);
try {
await apolloClient.query({
query: gql`
query Leak {
diagBooks(where: { writtenBy: 123 }) {
edges {
node {
id
}
}
}
}
`,
});
fail('should have thrown a validation error');
} catch (e) {
const error = getReturnedError(e);
expect(error.message).toContain('SecretAuthor');
}
});
it('should strip pointer target class names from non-nullable variable-coercion errors without master or maintenance key', async () => {
const schemaController = await parseServer.config.databaseController.loadSchema();
await schemaController.addClassIfNotExists('SecretAuthor', { name: { type: 'String' } });
await schemaController.addClassIfNotExists('ReqDiagBook', {
writtenBy: { type: 'Pointer', targetClass: 'SecretAuthor', required: true },
});
await resetGraphQLCache();
try {
await apolloClient.mutate({
mutation: gql`
mutation Create($input: CreateReqDiagBookInput!) {
createReqDiagBook(input: $input) {
reqDiagBook {
id
}
}
}
`,
variables: { input: { fields: { writtenBy: null } } },
});
fail('should have thrown a coercion error');
} catch (e) {
const error = getReturnedError(e);
// Expected non-nullable type "SecretAuthorPointerInput!" not to be null.
expect(error.message).not.toContain('SecretAuthor');
expect(JSON.stringify(error)).not.toContain('SecretAuthor');
}
});
it('should strip pointer target class names from variable-position validation errors without master or maintenance key', async () => {
await setupPointerSchema(parseServer);
try {
await apolloClient.query({
query: gql`
query Leak($x: String) {
diagBooks(where: { writtenBy: $x }) {
edges {
node {
id
}
}
}
}
`,
variables: { x: 'anything' },
});
fail('should have thrown a validation error');
} catch (e) {
const error = getReturnedError(e);
// Variable "$x" of type "String" used in position expecting type "SecretAuthorRelationWhereInput".
expect(error.message).not.toContain('SecretAuthor');
expect(JSON.stringify(error)).not.toContain('SecretAuthor');
}
});
it('should strip pointer target class names from mutation variable-position validation errors without master or maintenance key', async () => {
await setupPointerSchema(parseServer);
try {
await apolloClient.mutate({
mutation: gql`
mutation Create($x: String) {
createDiagBook(input: { fields: { writtenBy: { createAndLink: $x } } }) {
diagBook {
id
}
}
}
`,
variables: { x: 'anything' },
});
fail('should have thrown a validation error');
} catch (e) {
const error = getReturnedError(e);
// Variable "$x" of type "String" used in position expecting type "CreateSecretAuthorFieldsInput".
expect(error.message).not.toContain('SecretAuthor');
expect(JSON.stringify(error)).not.toContain('SecretAuthor');
}
});
it('should strip pointer target class names from output-field validation errors without master or maintenance key', async () => {
await setupPointerSchema(parseServer);
try {
await apolloClient.query({
query: gql`
query Leak {
diagBooks(where: {}) {
edges {
node {
writtenBy {
bogusSubField
}
}
}
}
}
`,
});
fail('should have thrown a validation error');
} catch (e) {
const error = getReturnedError(e);
// Cannot query field "bogusSubField" on type "SecretAuthor".
expect(error.message).not.toContain('SecretAuthor');
expect(JSON.stringify(error)).not.toContain('SecretAuthor');
}
});
it('should strip pointer target class names from scalar-leaf validation errors without master or maintenance key', async () => {
await setupPointerSchema(parseServer);
try {
await apolloClient.query({
query: gql`
query Leak {
diagBooks(where: {}) {
edges {
node {
writtenBy
}
}
}
}
`,
});
fail('should have thrown a validation error');
} catch (e) {
const error = getReturnedError(e);
// Field "writtenBy" of type "SecretAuthor" must have a selection of subfields.
expect(error.message).not.toContain('SecretAuthor');
expect(JSON.stringify(error)).not.toContain('SecretAuthor');
}
});
it('should keep pointer target class names in output-field errors with master key', async () => {
await setupPointerSchema(parseServer);
try {
await apolloClient.query({
query: gql`
query Leak {
diagBooks(where: {}) {
edges {
node {
writtenBy
}
}
}
}
`,
context: {
headers: {
'X-Parse-Master-Key': 'test',
},
},
});
fail('should have thrown a validation error');
} catch (e) {
const error = getReturnedError(e);
expect(error.message).toContain('SecretAuthor');
}
});
it('should strip pointer target class names from fragment-spread validation errors without master or maintenance key', async () => {
await setupPointerSchema(parseServer);
try {
await apolloClient.query({
query: gql`
query Leak {
diagBooks(where: {}) {
edges {
node {
writtenBy {
... on DiagBook {
id
}
}
}
}
}
}
`,
});
fail('should have thrown a validation error');
} catch (e) {
const error = getReturnedError(e);
// Fragment cannot be spread here as objects of type "SecretAuthor" can never be of type "DiagBook".
expect(error.message).not.toContain('SecretAuthor');
expect(JSON.stringify(error)).not.toContain('SecretAuthor');
}
});
it('should keep caller-referenced input type names in validation errors without master or maintenance key', async () => {
await setupPointerSchema(parseServer);
try {
await apolloClient.query({
query: gql`
query Leak($where: DiagBookWhereInput) {
diagBooks(where: $where) {
edges {
node {
id
}
}
}
}
`,
variables: { where: { nonexistentField: { equalTo: 1 } } },
});
fail('should have thrown a validation error');
} catch (e) {
const error = getReturnedError(e);
// The caller referenced DiagBookWhereInput in the operation text, so it is not a
// schema disclosure and must be preserved to keep validation feedback useful.
expect(error.message).toContain('DiagBookWhereInput');
}
});
});
+107
View File
@@ -1298,6 +1298,113 @@ describe('Installations', () => {
// TODO: Do we need to support _tombstone disabling of installations?
// TODO: Test deletion, badge increments
describe('access control for non-master clients', () => {
const anonymousHeaders = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
it('blocks the find operation for an unauthenticated client', async () => {
await rest.create(config, auth.nobody(config), '_Installation', {
installationId: '12345678-abcd-abcd-abcd-123456789abc',
deviceType: 'android',
});
let error;
try {
await request({
method: 'GET',
headers: anonymousHeaders,
url: 'http://localhost:8378/1/installations',
});
fail('find should have been rejected');
return;
} catch (e) {
error = e;
}
expect(error.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(error.data.error).toBe('Permission denied');
});
it('blocks the delete operation for an unauthenticated client', async () => {
const created = await rest.create(config, auth.nobody(config), '_Installation', {
installationId: '12345678-abcd-abcd-abcd-123456789abc',
deviceType: 'android',
});
let error;
try {
await request({
method: 'DELETE',
headers: anonymousHeaders,
url: 'http://localhost:8378/1/installations/' + created.response.objectId,
});
fail('delete should have been rejected');
return;
} catch (e) {
error = e;
}
expect(error.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(error.data.error).toBe('Permission denied');
// The row is still present: the anonymous delete did not take effect.
const remaining = await database.adapter.find('_Installation', installationSchema, {}, {});
expect(remaining.length).toBe(1);
});
it('blocks the find operation for an authenticated non-master user', async () => {
// Even a logged-in user cannot enumerate installations, so another
// device's objectId cannot be discovered through an authenticated session.
const user = await Parse.User.signUp('installation-acl-user', 'pass-12345678');
await rest.create(config, auth.nobody(config), '_Installation', {
installationId: '12345678-abcd-abcd-abcd-123456789abc',
deviceType: 'android',
});
let error;
try {
await request({
method: 'GET',
headers: {
...anonymousHeaders,
'X-Parse-Session-Token': user.getSessionToken(),
},
url: 'http://localhost:8378/1/installations',
});
fail('find should have been rejected');
return;
} catch (e) {
error = e;
}
expect(error.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(error.data.error).toBe('Permission denied');
});
it('blocks the delete operation for an authenticated non-master user', async () => {
const user = await Parse.User.signUp('installation-acl-user', 'pass-12345678');
const created = await rest.create(config, auth.nobody(config), '_Installation', {
installationId: '12345678-abcd-abcd-abcd-123456789abc',
deviceType: 'android',
});
let error;
try {
await request({
method: 'DELETE',
headers: {
...anonymousHeaders,
'X-Parse-Session-Token': user.getSessionToken(),
},
url: 'http://localhost:8378/1/installations/' + created.response.objectId,
});
fail('delete should have been rejected');
return;
} catch (e) {
error = e;
}
expect(error.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(error.data.error).toBe('Permission denied');
// The row is still present: the authenticated non-master delete did not take effect.
const remaining = await database.adapter.find('_Installation', installationSchema, {}, {});
expect(remaining.length).toBe(1);
});
});
describe('deviceToken deduplication on new install (no installationId match)', () => {
const { randomUUID } = require('crypto');
const installationSchema = {
+16
View File
@@ -5375,6 +5375,22 @@ describe('Parse.Query testing', () => {
expect(result.executionStats).not.toBeUndefined();
});
it_only_db('mongo')('does not run afterFind on explain results', async () => {
let afterFindCalled = false;
Parse.Cloud.afterFind('AfterFindExplain', () => {
afterFindCalled = true;
return []; // empty return would drop the explain plan if the trigger ran
});
const obj = new Parse.Object('AfterFindExplain');
await obj.save();
const query = new Parse.Query('AfterFindExplain');
query.equalTo('objectId', obj.id);
query.explain();
const result = await query.find({ useMasterKey: true });
expect(result.executionStats).not.toBeUndefined(); // plan passed through untouched
expect(afterFindCalled).toBe(false); // afterFind skipped for explain
});
it('should query with distinct within eachBatch and direct access enabled', async () => {
await reconfigureServer({
directAccess: true,
+65
View File
@@ -257,6 +257,71 @@ describe('Parse.Session', () => {
expect(newSession.createdWith.authProvider).toBeUndefined();
});
it('does not delete another user\'s session when creating a session via POST /classes/_Session', async () => {
const victim = await Parse.User.signUp('dedupvictim', 'password');
const attacker = await Parse.User.signUp('dedupattacker', 'password');
const victimId = victim.id;
const installationId = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d';
// Victim logs in on a known installation, creating a session with that installationId.
const victimLogin = await request({
method: 'POST',
url: 'http://localhost:8378/1/login',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': installationId,
'Content-Type': 'application/json',
},
body: { username: 'dedupvictim', password: 'password' },
});
const victimSessionToken = victimLogin.data.sessionToken;
// Another user creates a session while naming the victim as `user` and supplying
// the victim's installationId. The session dedup must not delete the victim's session.
await request({
method: 'POST',
url: 'http://localhost:8378/1/classes/_Session',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Session-Token': attacker.getSessionToken(),
'Content-Type': 'application/json',
},
body: {
user: { __type: 'Pointer', className: '_User', objectId: victimId },
installationId,
sessionToken: 'r:someothertoken',
},
});
// The victim's session on that installation must still exist...
const sessions = await request({
method: 'GET',
url: 'http://localhost:8378/1/classes/_Session',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
});
const victimSession = sessions.data.results.find(
s => s.installationId === installationId && s.user && s.user.objectId === victimId
);
expect(victimSession).toBeDefined();
// ...and the victim's session token must still authenticate.
const meResponse = await request({
method: 'GET',
url: 'http://localhost:8378/1/users/me',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Session-Token': victimSessionToken,
},
});
expect(meResponse.data.objectId).toBe(victimId);
});
it('should reject expiresAt when updating a session via PUT', async () => {
const user = await Parse.User.signUp('sessionupdateuser1', 'password');
const sessionToken = user.getSessionToken();
+60
View File
@@ -6101,6 +6101,66 @@ describe('Vulnerabilities', () => {
expect(contextAfterDelete).toBeDefined();
expect(contextAfterDelete.isAdmin).toBeUndefined();
});
it('does not expose Object.prototype on beforeFind trigger context', async () => {
// getRequestQueryObject builds the beforeFind trigger request. Its context must be
// prototype-isolated like every other trigger path (getRequestObject), so a polluted
// Object.prototype cannot leak into the request.context read by Cloud Code.
let contextProto;
let contextValue;
Parse.Cloud.beforeFind('ContextTest', req => {
contextProto = Object.getPrototypeOf(req.context);
contextValue = req.context.foo;
});
const query = new Parse.Query('ContextTest');
await query.find({ context: { foo: 'bar' } });
expect(contextValue).toBe('bar');
expect(contextProto).toBeNull();
});
it('isolates beforeFind trigger context from Object.prototype pollution', async () => {
// Simulate a separate prototype-pollution issue elsewhere in the process and verify the
// beforeFind trigger context does not inherit the polluted property.
const probe = '__parseServerBeforeFindContextProbe';
let inheritedProbe;
Parse.Cloud.beforeFind('ContextTest', req => {
inheritedProbe = req.context[probe];
});
Object.defineProperty(Object.prototype, probe, {
value: true,
configurable: true,
enumerable: false,
writable: true,
});
try {
const query = new Parse.Query('ContextTest');
await query.find({ context: { foo: 'bar' } });
} finally {
delete Object.prototype[probe];
}
expect(inheritedProbe).toBeUndefined();
});
it('propagates beforeFind context mutations to afterFind with prototype isolation', async () => {
// Regression guard for the copy + write-back fix: beforeFind and afterFind must still
// share context mutations (as documented), and both trigger contexts must be isolated.
let beforeFindProto;
let afterFindProto;
let afterFindValue;
Parse.Cloud.beforeFind('ContextTest', req => {
beforeFindProto = Object.getPrototypeOf(req.context);
req.context.injected = 'from-beforeFind';
});
Parse.Cloud.afterFind('ContextTest', req => {
afterFindProto = Object.getPrototypeOf(req.context);
afterFindValue = req.context.injected;
});
const query = new Parse.Query('ContextTest');
await query.find({ context: { foo: 'bar' } });
expect(beforeFindProto).toBeNull();
expect(afterFindProto).toBeNull();
expect(afterFindValue).toBe('from-beforeFind');
});
});
describe('(GHSA-hpm8-9qx6-jvwv) Ranged file download bypasses afterFind(Parse.File) trigger and validators', () => {
+48 -2
View File
@@ -1,6 +1,48 @@
const mongodb = require('mongodb');
const Collection = mongodb.Collection;
// Query operators that require a geospatial index and therefore trigger
// on-demand `2d` index creation. `$geoWithin` / `$geoIntersects` are intentionally
// excluded: they can run as a collection scan and never raise a "no index" error.
const GEO_INDEX_QUERY_OPERATORS = ['$nearSphere', '$near', '$geoNear'];
// Find the field in a Mongo query document that is constrained by a geo operator
// requiring a geospatial index. Returns the field name (e.g. 'location'), or
// undefined if none is found. Used as the reliable source of truth for on-demand
// geo index creation, since the MongoDB error message that used to carry the field
// name (`... field=<name> ...`) was dropped in MongoDB 8.3+.
//
// A geo-near expression must be top-level or inside `$and`: MongoDB rejects it inside
// `$or` / `$nor` ("geo $near must be top-level expr") and forbids more than one per
// query ("Too many geoNear expressions"). So there is at most one field to find, and
// `$and` is the only combinator we need to recurse into.
export function findGeoIndexField(query) {
if (!query || typeof query !== 'object') {
return undefined;
}
for (const field of Object.keys(query)) {
const value = query[field];
// Recurse into `$and`, which holds an array of sub-queries.
if (field === '$and' && Array.isArray(value)) {
for (const subQuery of value) {
const found = findGeoIndexField(subQuery);
if (found) {
return found;
}
}
continue;
}
if (
value &&
typeof value === 'object' &&
GEO_INDEX_QUERY_OPERATORS.some(op => Object.prototype.hasOwnProperty.call(value, op))
) {
return field;
}
}
return undefined;
}
export default class MongoCollection {
_mongoCollection: Collection;
@@ -51,8 +93,12 @@ export default class MongoCollection {
if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) {
throw error;
}
// Figure out what key needs an index
const key = error.message.match(/field=([A-Za-z_0-9]+) /)[1];
// Figure out which field needs a geo index.
// Older MongoDB embeds the field name in the error message (`... field=<name> ...`);
// MongoDB 8.3+ shortened the message to `unable to find index for $geoNear query`
// and no longer includes it, so fall back to reading the field from the query itself.
const messageMatch = error.message.match(/field=([A-Za-z_0-9]+) /);
const key = (messageMatch && messageMatch[1]) || findGeoIndexField(query);
if (!key) {
throw error;
}
+81 -4
View File
@@ -122,8 +122,82 @@ const stripSchemaCoercionIdentifiers = message =>
)
: message;
const stripSchemaIdentifiers = message =>
stripSchemaCoercionIdentifiers(stripSchemaSuggestion(message));
// graphql-js also emits base coercion / validation messages that name a nested input
// TYPE without a "Did you mean" clause, so neither strip above reaches them. For a
// Pointer or Relation field the generated input type name embeds the pointer's TARGET
// class (`<Target>PointerInput`, `<Target>RelationWhereInput`, `Create<Target>FieldsInput`)
// — a class the caller never referenced and cannot derive from the field name they
// supplied — so these templates disclose a schema class name to a caller who has only the
// public application id. Redact the quoted type identifier from those templates UNLESS the
// caller referenced it in the operation text: a type name the caller wrote in the operation
// (e.g. `$where: UserWhereInput`) is not a disclosure, and preserving it keeps the message
// ('... is not defined by type "UserWhereInput".') useful. When the operation text is
// unavailable the identifier is redacted (fail closed).
const stripSchemaTypeIdentifiers = (message, operationText) => {
if (typeof message !== 'string') { return message; }
// A generated type identifier counts as "referenced" (and therefore not a disclosure) only if
// the caller wrote it as a whole token in the operation text. Tokenize the operation on
// non-identifier characters and compare exact tokens rather than building a RegExp from the
// captured name: this avoids substring false-matches (e.g. preserving "AuthorPointerInput"
// because the operation contains "SecretAuthorPointerInput") and any regex injection/ReDoS from
// an unusual captured name. GraphQL list/non-null wrappers ("[", "]", "!") are stripped from the
// captured name so e.g. "SecretAuthorPointerInput!" still matches "$x: SecretAuthorPointerInput!".
// When the operation text is unavailable the type is treated as not referenced (fail closed).
const referencedTokens =
typeof operationText === 'string'
? new Set(operationText.split(/[^_A-Za-z0-9]+/).filter(Boolean))
: new Set();
const isReferenced = typeName => referencedTokens.has(typeName.replace(/[[\]!]/g, ''));
return message
// Input coercion / ValuesOfCorrectTypeRule (variables and inline literals).
.replace(/Expected value of type "([^"]+)"/g, (match, typeName) =>
isReferenced(typeName) ? match : 'Expected value of the correct type'
)
.replace(/Expected type "([^"]+)" to be an object\./g, (match, typeName) =>
isReferenced(typeName) ? match : 'Expected an object.'
)
.replace(/Expected non-nullable type "([^"]+)" not to be null\./g, (match, typeName) =>
isReferenced(typeName) ? match : 'Expected a non-null value.'
)
.replace(/ is not defined by type "([^"]+)"\./g, (match, typeName) =>
isReferenced(typeName) ? match : ' is not defined.'
)
// VariablesInAllowedPositionRule: the position type is the pointer/relation target
// input type; the caller only wrote their own variable's declared type.
.replace(/ used in position expecting type "([^"]+)"\./g, (match, typeName) =>
isReferenced(typeName) ? match : ' used in position expecting a different type.'
)
// FieldsOnCorrectTypeRule: descending into a Pointer/Relation output field names its
// target output object type.
.replace(/Cannot query field ("[^"]*") on type "([^"]+)"\./g, (match, fieldName, typeName) =>
isReferenced(typeName) ? match : `Cannot query field ${fieldName}.`
)
// ScalarLeafsRule: selecting a Pointer/Relation output field with no sub-selection names
// its target output object type.
.replace(
/Field ("[^"]*") of type "([^"]+)" must have a selection of subfields\./g,
(match, fieldName, typeName) =>
isReferenced(typeName) ? match : `Field ${fieldName} must have a selection of subfields.`
)
// PossibleFragmentSpreadsRule: an inline/named fragment on an incompatible type inside a
// Pointer/Relation output field names the target output object type (the parent type).
// Redact each type token the caller did not reference; when both are referenced the
// reconstruction is identical to the original message.
.replace(
/objects of type "([^"]+)" can never be of type "([^"]+)"\./g,
(match, parentType, fragType) => {
const parent = isReferenced(parentType) ? `type "${parentType}"` : 'the parent type';
const frag = isReferenced(fragType) ? `type "${fragType}"` : 'the given type';
return `objects of ${parent} can never be of ${frag}.`;
}
);
};
const stripSchemaIdentifiers = (message, operationText) =>
stripSchemaTypeIdentifiers(
stripSchemaCoercionIdentifiers(stripSchemaSuggestion(message)),
operationText
);
const SchemaSuggestionsControlPlugin = (publicIntrospection) => ({
requestDidStart: async (requestContext) => ({
@@ -144,10 +218,13 @@ const SchemaSuggestionsControlPlugin = (publicIntrospection) => ({
: body?.kind === 'incremental'
? body.initialResult.errors
: undefined;
const operationText = requestContext.request?.query;
errors?.forEach(error => {
error.message = stripSchemaIdentifiers(error.message);
error.message = stripSchemaIdentifiers(error.message, operationText);
if (Array.isArray(error.extensions?.stacktrace)) {
error.extensions.stacktrace = error.extensions.stacktrace.map(stripSchemaIdentifiers);
error.extensions.stacktrace = error.extensions.stacktrace.map(message =>
stripSchemaIdentifiers(message, operationText)
);
}
});
},
+3 -3
View File
@@ -346,13 +346,13 @@ module.exports.ParseServerOptions = {
},
liveQuery: {
env: 'PARSE_SERVER_LIVE_QUERY',
help: "parse-server's LiveQuery configuration object",
help: "Configuration for LiveQuery on this Parse Server, for example `{ classNames: ['MyClass'] }`. `classNames` lists the classes that publish create/update/delete events to subscribers; without it no events are pushed, even while a LiveQuery server is running. Combine with `startLiveQueryServer` to run a LiveQuery server.",
action: parsers.objectParser,
type: 'LiveQueryOptions',
},
liveQueryServerOptions: {
env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS',
help: 'Live query server configuration options (will start the liveQuery server)',
help: 'Configuration options for the LiveQuery server. Providing this also starts the LiveQuery server (like `startLiveQueryServer`); events are still only published for the classes set in `liveQuery.classNames`.',
action: parsers.objectParser,
type: 'LiveQueryServerOptions',
},
@@ -639,7 +639,7 @@ module.exports.ParseServerOptions = {
},
startLiveQueryServer: {
env: 'PARSE_SERVER_START_LIVE_QUERY_SERVER',
help: 'Starts the liveQuery server',
help: 'Starts a LiveQuery server alongside this Parse Server. Events are only delivered for the classes set in `liveQuery.classNames`, so a minimal working setup is `liveQuery: { classNames: [...] }` together with `startLiveQueryServer: true`.',
action: parsers.booleanParser,
},
trustProxy: {
+3 -3
View File
@@ -65,8 +65,8 @@
* @property {InstallationOptions} installation Options controlling how Parse Server deduplicates `_Installation` records that share the same `deviceToken`.
* @property {String} javascriptKey Key for the Javascript SDK
* @property {Boolean} jsonLogs Log as structured JSON objects
* @property {LiveQueryOptions} liveQuery parse-server's LiveQuery configuration object
* @property {LiveQueryServerOptions} liveQueryServerOptions Live query server configuration options (will start the liveQuery server)
* @property {LiveQueryOptions} liveQuery Configuration for LiveQuery on this Parse Server, for example `{ classNames: ['MyClass'] }`. `classNames` lists the classes that publish create/update/delete events to subscribers; without it no events are pushed, even while a LiveQuery server is running. Combine with `startLiveQueryServer` to run a LiveQuery server.
* @property {LiveQueryServerOptions} liveQueryServerOptions Configuration options for the LiveQuery server. Providing this also starts the LiveQuery server (like `startLiveQueryServer`); events are still only published for the classes set in `liveQuery.classNames`.
* @property {Adapter<LoggerAdapter>} loggerAdapter Adapter module for the logging sub-system
* @property {String} logLevel Sets the level for logs
* @property {LogLevels} logLevels (Optional) Overrides the log levels used internally by Parse Server to log events.
@@ -115,7 +115,7 @@
* @property {String} serverURL The URL to Parse Server.<br><br>⚠️ Certain server features or adapters may require Parse Server to be able to call itself by making requests to the URL set in `serverURL`. If a feature requires this, it is mentioned in the documentation. In that case ensure that the URL is accessible from the server itself.
* @property {Number} sessionLength Session duration, in seconds, defaults to 1 year
* @property {Boolean} silent Disables console output
* @property {Boolean} startLiveQueryServer Starts the liveQuery server
* @property {Boolean} startLiveQueryServer Starts a LiveQuery server alongside this Parse Server. Events are only delivered for the classes set in `liveQuery.classNames`, so a minimal working setup is `liveQuery: { classNames: [...] }` together with `startLiveQueryServer: true`.
* @property {Any} trustProxy The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.
* @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields
* @property {Boolean} verbose Set the logging to verbose
+3 -3
View File
@@ -288,7 +288,7 @@ export interface ParseServerOptions {
/* custom pages for password validation and reset
:DEFAULT: {} */
customPages: ?CustomPagesOptions;
/* parse-server's LiveQuery configuration object */
/* Configuration for LiveQuery on this Parse Server, for example `{ classNames: ['MyClass'] }`. `classNames` lists the classes that publish create/update/delete events to subscribers; without it no events are pushed, even while a LiveQuery server is running. Combine with `startLiveQueryServer` to run a LiveQuery server. */
liveQuery: ?LiveQueryOptions;
/* Session duration, in seconds, defaults to 1 year
:DEFAULT: 31536000 */
@@ -347,9 +347,9 @@ export interface ParseServerOptions {
/* The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.
:DEFAULT: false */
trustProxy: ?any;
/* Starts the liveQuery server */
/* Starts a LiveQuery server alongside this Parse Server. Events are only delivered for the classes set in `liveQuery.classNames`, so a minimal working setup is `liveQuery: { classNames: [...] }` together with `startLiveQueryServer: true`. */
startLiveQueryServer: ?boolean;
/* Live query server configuration options (will start the liveQuery server) */
/* Configuration options for the LiveQuery server. Providing this also starts the LiveQuery server (like `startLiveQueryServer`); events are still only published for the classes set in `liveQuery.classNames`. */
liveQueryServerOptions: ?LiveQueryServerOptions;
/* Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.
:ENV: PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS
+2 -2
View File
@@ -1121,8 +1121,8 @@ _UnsafeRestQuery.prototype.runAfterFindTrigger = function () {
if (!hasAfterFindHook) {
return Promise.resolve();
}
// Skip Aggregate and Distinct Queries
if (this.findOptions.pipeline || this.findOptions.distinct) {
// Skip Aggregate, Distinct and Explain Queries
if (this.findOptions.pipeline || this.findOptions.distinct || this.findOptions.explain) {
return Promise.resolve();
}
+8
View File
@@ -1151,6 +1151,14 @@ RestWrite.prototype.deleteEmailResetTokenIfNeeded = function () {
};
RestWrite.prototype.destroyDuplicatedSessions = function () {
// Skip if the response is already set, matching the other write-pipeline steps
// (runDatabaseOperation, runAfterSaveTrigger). A non-master POST /classes/_Session
// create has handleSession() set this.response before this runs, so this guard
// prevents the dedup delete from acting on the client-supplied `user`/`installationId`
// rather than on the server-generated session data.
if (this.response) {
return;
}
// Only for _Session, and at creation time
if (this.className != '_Session' || this.query) {
return;
+12 -4
View File
@@ -344,7 +344,9 @@ export function getRequestQueryObject(triggerType, auth, query, count, config, c
isGet,
headers: config.headers,
ip: config.ip,
context: context || {},
// Set a copy of the context on the request object, with a null prototype so a
// polluted Object.prototype cannot leak into the trigger context
context: Object.assign(Object.create(null), context || {}),
config,
};
@@ -612,6 +614,12 @@ export function maybeRunQueryTrigger(
})
.then(
result => {
// Propagate any context mutations made by the trigger back to the shared context,
// mirroring the write-back for other trigger types in maybeRunTrigger. This preserves
// beforeFind -> afterFind context propagation now that the request context is a copy.
if (context) {
Object.assign(context, requestObject.context);
}
let queryResult = parseQuery;
if (result && result instanceof Parse.Query) {
queryResult = result;
@@ -815,7 +823,7 @@ async function builtInTriggerValidator(options, request, auth) {
requiredParam(key);
}
} else {
const optionPromises = [];
const optionValidations = [];
for (const key in options.fields) {
const opt = options.fields[key];
let val = params[key];
@@ -850,12 +858,12 @@ async function builtInTriggerValidator(options, request, auth) {
}
}
if (opt.options) {
optionPromises.push(validateOptions(opt, key, val));
optionValidations.push([opt, key, val]);
}
}
}
}
await Promise.all(optionPromises);
await Promise.all(optionValidations.map(([o, k, v]) => validateOptions(o, k, v)));
}
let userRoles = options.requireAnyUserRoles;
let requireAllRoles = options.requireAllUserRoles;