Compare commits

..
Author SHA1 Message Date
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
semantic-release-bot 3fa545f590 chore(release): 9.10.0-alpha.5 [skip ci]
# [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)

### Bug Fixes

* 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)
2026-07-10 15:44:49 +00:00
Manuel d96c945b6d fix: 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) 2026-07-10 17:43:50 +02:00
semantic-release-bot 293f60e5f3 chore(release): 9.10.0-alpha.4 [skip ci]
# [9.10.0-alpha.4](https://github.com/parse-community/parse-server/compare/9.10.0-alpha.3...9.10.0-alpha.4) (2026-07-07)

### Bug Fixes

* 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))
2026-07-07 21:51:49 +00:00
Manuel 2625489a27 fix: 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) 2026-07-07 23:50:55 +02:00
semantic-release-bot 11010cc02b chore(release): 9.10.0-alpha.3 [skip ci]
# [9.10.0-alpha.3](https://github.com/parse-community/parse-server/compare/9.10.0-alpha.2...9.10.0-alpha.3) (2026-07-07)

### Bug Fixes

* 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))
2026-07-07 13:38:05 +00:00
Daniel 459786fd41 fix: NumberOrBoolean config option (cluster) value not coerced from env/CLI (#10531) 2026-07-07 15:37:06 +02:00
15 changed files with 1043 additions and 26 deletions
+4
View File
@@ -187,6 +187,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
+42
View File
@@ -1,3 +1,45 @@
# [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)
### Bug Fixes
* 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)
# [9.10.0-alpha.4](https://github.com/parse-community/parse-server/compare/9.10.0-alpha.3...9.10.0-alpha.4) (2026-07-07)
### Bug Fixes
* 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))
# [9.10.0-alpha.3](https://github.com/parse-community/parse-server/compare/9.10.0-alpha.2...9.10.0-alpha.3) (2026-07-07)
### Bug Fixes
* 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))
# [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)
+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)
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "parse-server",
"version": "9.10.0-alpha.2",
"version": "9.10.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "parse-server",
"version": "9.10.0-alpha.2",
"version": "9.10.0",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "9.10.0-alpha.2",
"version": "9.10.0",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
+1 -1
View File
@@ -177,7 +177,7 @@ function mapperFor(elt, t) {
return wrap(t.identifier('moduleOrObjectParser'));
}
if (type == 'NumberOrBoolean') {
return wrap(t.identifier('numberOrBooleanParser'));
return t.callExpression(wrap(t.identifier('numberOrBoolParser')), [t.stringLiteral(elt.name)]);
}
if (type == 'NumberOrString') {
return t.callExpression(wrap(t.identifier('numberOrStringParser')), [t.stringLiteral(elt.name)]);
+10 -2
View File
@@ -178,7 +178,7 @@ describe('definitions', () => {
if (typeof definition.required !== 'undefined') {
expect(typeof definition.required).toBe('boolean');
}
if (typeof definition.action !== 'undefined') {
if ('action' in definition) {
expect(typeof definition.action).toBe('function');
}
}
@@ -189,6 +189,14 @@ describe('definitions', () => {
definitions.facebookAppIds.action();
}).toThrow();
});
it('should coerce the NumberOrBoolean cluster option value', () => {
const action = definitions.cluster.action;
expect(typeof action).toBe('function');
expect(action('2')).toBe(2);
expect(action('true')).toBe(true);
expect(action('false')).toBe(false);
});
});
describe('LiveQuery definitions', () => {
@@ -203,7 +211,7 @@ describe('LiveQuery definitions', () => {
if (typeof definition.required !== 'undefined') {
expect(typeof definition.required).toBe('boolean');
}
if (typeof definition.action !== 'undefined') {
if ('action' in definition) {
expect(typeof definition.action).toBe('function');
}
}
+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/);
});
});
});
+603
View File
@@ -1125,6 +1125,609 @@ describe('ParseGraphQLServer', () => {
expect(message).toContain('health');
}
});
const getReturnedError = e =>
(e.networkError && e.networkError.result && e.networkError.result.errors[0]) ||
(e.graphQLErrors && e.graphQLErrors[0]);
it('should strip "Did you mean" enum suggestions from variable-coercion errors without master or maintenance key', async () => {
Parse.Cloud.define('secretAdminTask', () => 'ok');
try {
await apolloClient.mutate({
mutation: gql`
mutation LeakFunction($input: CallCloudCodeInput!) {
callCloudCode(input: $input) {
result
}
}
`,
variables: { input: { functionName: 'secretAdminTas', params: {} } },
});
fail('should have thrown a coercion error');
} catch (e) {
const error = getReturnedError(e);
expect(error.message).toContain('CloudCodeFunction');
expect(error.message).not.toMatch(/Did you mean/);
expect(error.message).not.toContain('secretAdminTask');
// The cloud function name must not leak through any returned field
// (e.g. a stacktrace duplicated from the original message in non-production).
expect(JSON.stringify(error)).not.toContain('secretAdminTask');
}
});
it('should strip "Did you mean" field suggestions from variable-coercion errors without master or maintenance key', async () => {
try {
await apolloClient.query({
query: gql`
query Leak($where: UserWhereInput) {
users(where: $where) {
edges {
node {
id
}
}
}
}
`,
variables: { where: { usernme: { equalTo: 'victim' } } },
});
fail('should have thrown a coercion error');
} catch (e) {
const error = getReturnedError(e);
expect(error.message).toContain('UserWhereInput');
expect(error.message).not.toMatch(/Did you mean/);
// JSON.stringify escapes embedded quotes, so assert against the bare
// identifier to reliably catch a leak duplicated into extensions.stacktrace.
expect(error.message).not.toContain('username');
expect(JSON.stringify(error)).not.toContain('username');
}
});
it('should keep "Did you mean" enum suggestions in variable-coercion errors with master key', async () => {
Parse.Cloud.define('secretAdminTask', () => 'ok');
try {
await apolloClient.mutate({
mutation: gql`
mutation LeakFunction($input: CallCloudCodeInput!) {
callCloudCode(input: $input) {
result
}
}
`,
variables: { input: { functionName: 'secretAdminTas', params: {} } },
context: {
headers: {
'X-Parse-Master-Key': 'test',
},
},
});
fail('should have thrown a coercion error');
} catch (e) {
const error = getReturnedError(e);
expect(error.message).toMatch(/Did you mean/);
expect(error.message).toContain('secretAdminTask');
}
});
it('should keep "Did you mean" enum suggestions in variable-coercion errors when public introspection is enabled', async () => {
const parseServer = await reconfigureServer();
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });
Parse.Cloud.define('secretAdminTask', () => 'ok');
try {
await apolloClient.mutate({
mutation: gql`
mutation LeakFunction($input: CallCloudCodeInput!) {
callCloudCode(input: $input) {
result
}
}
`,
variables: { input: { functionName: 'secretAdminTas', params: {} } },
});
fail('should have thrown a coercion error');
} catch (e) {
const error = getReturnedError(e);
expect(error.message).toMatch(/Did you mean/);
expect(error.message).toContain('secretAdminTask');
}
});
it('should strip required-field names from base coercion errors without master or maintenance key', async () => {
const schemaController = await parseServer.config.databaseController.loadSchema();
await schemaController.addClassIfNotExists('TestReqClass', {
secretRequiredField: { type: 'String', required: true },
});
await resetGraphQLCache();
try {
await apolloClient.mutate({
mutation: gql`
mutation Create($input: CreateTestReqClassInput!) {
createTestReqClass(input: $input) {
testReqClass {
id
}
}
}
`,
variables: { input: { fields: {} } },
});
fail('should have thrown a coercion error');
} catch (e) {
const error = getReturnedError(e);
// The base graphql-js "... was not provided." coercion message carries no
// "Did you mean" clause, so it discloses the required custom field name to a
// caller who only has the public application id. It must be redacted.
expect(error.message).not.toContain('secretRequiredField');
// The message is duplicated into extensions.stacktrace in non-production;
// ensure the identifier does not leak through any returned field.
expect(JSON.stringify(error)).not.toContain('secretRequiredField');
}
});
it('should keep required-field names in base coercion errors with master key', async () => {
const schemaController = await parseServer.config.databaseController.loadSchema();
await schemaController.addClassIfNotExists('TestReqClass', {
secretRequiredField: { type: 'String', required: true },
});
await resetGraphQLCache();
try {
await apolloClient.mutate({
mutation: gql`
mutation Create($input: CreateTestReqClassInput!) {
createTestReqClass(input: $input) {
testReqClass {
id
}
}
}
`,
variables: { input: { fields: {} } },
context: {
headers: {
'X-Parse-Master-Key': 'test',
},
},
});
fail('should have thrown a coercion error');
} catch (e) {
const error = getReturnedError(e);
expect(error.message).toContain('secretRequiredField');
}
});
it('should keep required-field names in base coercion errors when public introspection is enabled', async () => {
const parseServer = await reconfigureServer();
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });
const schemaController = await parseServer.config.databaseController.loadSchema();
await schemaController.addClassIfNotExists('TestReqClass', {
secretRequiredField: { type: 'String', required: true },
});
await resetGraphQLCache();
try {
await apolloClient.mutate({
mutation: gql`
mutation Create($input: CreateTestReqClassInput!) {
createTestReqClass(input: $input) {
testReqClass {
id
}
}
}
`,
variables: { input: { fields: {} } },
});
fail('should have thrown a coercion error');
} catch (e) {
const error = getReturnedError(e);
expect(error.message).toContain('secretRequiredField');
}
});
it('should strip required-field names from inline-literal coercion errors without master or maintenance key', async () => {
const schemaController = await parseServer.config.databaseController.loadSchema();
await schemaController.addClassIfNotExists('TestReqClass', {
secretRequiredField: { type: 'String', required: true },
});
await resetGraphQLCache();
try {
// Input written inline in the operation (not via a variable) is validated by
// ValuesOfCorrectTypeRule, which emits a type-qualified message
// ('Field "<Type>.<field>" of required type ...'), disclosing both the generated
// input type name (which embeds the class name) and the required field name.
await apolloClient.mutate({
mutation: gql`
mutation Create {
createTestReqClass(input: { fields: {} }) {
testReqClass {
id
}
}
}
`,
});
fail('should have thrown a validation error');
} catch (e) {
const error = getReturnedError(e);
expect(error.message).not.toContain('secretRequiredField');
expect(error.message).not.toContain('CreateTestReqClass');
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');
}
});
});
+5 -4
View File
@@ -81,9 +81,10 @@ describe('buildConfigDefinitions', () => {
expect(result.property.name).toBe('moduleOrObjectParser');
});
it('should return numberOrBooleanParser for NumberOrBoolean GenericTypeAnnotation', () => {
it('should return numberOrBoolParser call expression for NumberOrBoolean GenericTypeAnnotation', () => {
const mockElement = {
type: 'GenericTypeAnnotation',
name: 'cluster',
typeAnnotation: {
id: {
name: 'NumberOrBoolean',
@@ -93,9 +94,9 @@ describe('buildConfigDefinitions', () => {
const result = mapperFor(mockElement, t);
expect(t.isMemberExpression(result)).toBe(true);
expect(result.object.name).toBe('parsers');
expect(result.property.name).toBe('numberOrBooleanParser');
expect(t.isCallExpression(result)).toBe(true);
expect(result.callee.property.name).toBe('numberOrBoolParser');
expect(result.arguments[0].value).toBe('cluster');
});
it('should return numberOrStringParser call expression for NumberOrString GenericTypeAnnotation', () => {
+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;
}
+126 -12
View File
@@ -90,15 +90,118 @@ const IntrospectionControlPlugin = (publicIntrospection) => ({
});
// graphql-js validation rules (FieldsOnCorrectTypeRule, KnownArgumentNamesRule,
// KnownTypeNamesRule, ...) embed "Did you mean ...?" hints sourced from the live
// schema in their error messages. Those messages are returned to the caller
// before didResolveOperation runs, so they sidestep IntrospectionControlPlugin
// and disclose schema identifiers the introspection guard is meant to hide.
// Strip the hint suffix for callers that are not allowed to introspect.
// graphql-js embeds "Did you mean ...?" hints sourced from the live schema in
// its error messages. They are produced in two distinct phases:
// - validation rules (FieldsOnCorrectTypeRule, KnownArgumentNamesRule,
// KnownTypeNamesRule, ...), and
// - variable coercion (unknown enum values, unknown input-object fields),
// which runs during execution, after validation.
// All of these are returned to the caller and disclose schema identifiers (Cloud
// Code function names, class and field names) that the introspection guard is
// meant to hide. Strip the hint suffix from every returned error — including the
// copy graphql-js duplicates into extensions.stacktrace in non-production — for
// callers that are not allowed to introspect.
const stripSchemaSuggestion = message =>
typeof message === 'string' ? message.replace(/ ?Did you mean(.+?)\?$/, '') : message;
// graphql-js also emits a base input-coercion message that names a schema
// identifier WITHOUT a "Did you mean" clause, so the suggestion strip above
// cannot reach it: when a required custom input field is omitted, coerceInputValue
// returns 'Field "<name>" of required type "<type>" was not provided.', disclosing
// a field name the caller never supplied. Redact the quoted identifiers from this
// template while preserving the error shape, for callers that are not allowed to
// introspect. The sibling coercion messages ('... is not defined by type "<type>".',
// 'Expected type "<type>" to be an object.') are intentionally left intact: they
// only echo an input type name the caller already referenced in the operation, so
// they disclose nothing the caller did not already provide.
const stripSchemaCoercionIdentifiers = message =>
typeof message === 'string'
? message.replace(
/Field "[^"]*" of required type "[^"]*" was not provided\./g,
'Field of required type was not provided.'
)
: 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) => ({
validationDidStart: async () => {
willSendResponse: async () => {
if (publicIntrospection) {
return;
}
@@ -108,11 +211,22 @@ const SchemaSuggestionsControlPlugin = (publicIntrospection) => ({
if (isMasterOrMaintenance) {
return;
}
return async (validationErrors) => {
validationErrors?.forEach(error => {
error.message = error.message.replace(/ ?Did you mean(.+?)\?$/, '');
});
};
const body = requestContext.response?.body;
const errors =
body?.kind === 'single'
? body.singleResult.errors
: body?.kind === 'incremental'
? body.initialResult.errors
: undefined;
const operationText = requestContext.request?.query;
errors?.forEach(error => {
error.message = stripSchemaIdentifiers(error.message, operationText);
if (Array.isArray(error.extensions?.stacktrace)) {
error.extensions.stacktrace = error.extensions.stacktrace.map(message =>
stripSchemaIdentifiers(message, operationText)
);
}
});
},
}),
});
+1 -1
View File
@@ -139,7 +139,7 @@ module.exports.ParseServerOptions = {
cluster: {
env: 'PARSE_SERVER_CLUSTER',
help: 'Run with cluster, optionally set the number of processes default to os.cpus().length',
action: parsers.numberOrBooleanParser,
action: parsers.numberOrBoolParser('cluster'),
},
collectionPrefix: {
env: 'PARSE_SERVER_COLLECTION_PREFIX',
+9 -1
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;