mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4d6c5a16d |
@@ -187,10 +187,6 @@ 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
|
||||
|
||||
@@ -1,66 +1,3 @@
|
||||
## [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)
|
||||
|
||||
|
||||
### 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)
|
||||
|
||||
|
||||
|
||||
@@ -1,33 +1,3 @@
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
Generated
+577
-683
File diff suppressed because it is too large
Load Diff
+7
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "9.10.1-alpha.3",
|
||||
"version": "9.10.0-alpha.2",
|
||||
"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.16.0",
|
||||
"follow-redirects": "1.15.11",
|
||||
"graphql": "16.13.2",
|
||||
"graphql-list-fields": "2.0.4",
|
||||
"graphql-relay": "0.10.2",
|
||||
@@ -60,13 +60,13 @@
|
||||
"tv4": "1.3.0",
|
||||
"winston": "3.19.0",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.21.0"
|
||||
"ws": "8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "3.0.0",
|
||||
"@apollo/client": "3.13.8",
|
||||
"@babel/cli": "7.28.6",
|
||||
"@babel/core": "7.29.6",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/eslint-parser": "7.28.6",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.27.1",
|
||||
@@ -82,12 +82,12 @@
|
||||
"all-node-versions": "13.0.1",
|
||||
"apollo-upload-client": "18.0.1",
|
||||
"clean-jsdoc-theme": "4.3.0",
|
||||
"cross-env": "10.1.0",
|
||||
"cross-env": "7.0.3",
|
||||
"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.6",
|
||||
"form-data": "4.0.5",
|
||||
"globals": "17.3.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"jasmine": "6.1.0",
|
||||
@@ -106,7 +106,7 @@
|
||||
"prettier": "3.8.1",
|
||||
"semantic-release": "25.0.3",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.59.1",
|
||||
"typescript-eslint": "8.58.0",
|
||||
"yaml": "2.8.3"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -177,7 +177,7 @@ function mapperFor(elt, t) {
|
||||
return wrap(t.identifier('moduleOrObjectParser'));
|
||||
}
|
||||
if (type == 'NumberOrBoolean') {
|
||||
return t.callExpression(wrap(t.identifier('numberOrBoolParser')), [t.stringLiteral(elt.name)]);
|
||||
return wrap(t.identifier('numberOrBooleanParser'));
|
||||
}
|
||||
if (type == 'NumberOrString') {
|
||||
return t.callExpression(wrap(t.identifier('numberOrStringParser')), [t.stringLiteral(elt.name)]);
|
||||
|
||||
+2
-10
@@ -178,7 +178,7 @@ describe('definitions', () => {
|
||||
if (typeof definition.required !== 'undefined') {
|
||||
expect(typeof definition.required).toBe('boolean');
|
||||
}
|
||||
if ('action' in definition) {
|
||||
if (typeof definition.action !== 'undefined') {
|
||||
expect(typeof definition.action).toBe('function');
|
||||
}
|
||||
}
|
||||
@@ -189,14 +189,6 @@ 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', () => {
|
||||
@@ -211,7 +203,7 @@ describe('LiveQuery definitions', () => {
|
||||
if (typeof definition.required !== 'undefined') {
|
||||
expect(typeof definition.required).toBe('boolean');
|
||||
}
|
||||
if ('action' in definition) {
|
||||
if (typeof definition.action !== 'undefined') {
|
||||
expect(typeof definition.action).toBe('function');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
'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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1125,609 +1125,6 @@ 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1298,113 +1298,6 @@ 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 = {
|
||||
|
||||
@@ -257,71 +257,6 @@ 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();
|
||||
|
||||
@@ -81,10 +81,9 @@ describe('buildConfigDefinitions', () => {
|
||||
expect(result.property.name).toBe('moduleOrObjectParser');
|
||||
});
|
||||
|
||||
it('should return numberOrBoolParser call expression for NumberOrBoolean GenericTypeAnnotation', () => {
|
||||
it('should return numberOrBooleanParser for NumberOrBoolean GenericTypeAnnotation', () => {
|
||||
const mockElement = {
|
||||
type: 'GenericTypeAnnotation',
|
||||
name: 'cluster',
|
||||
typeAnnotation: {
|
||||
id: {
|
||||
name: 'NumberOrBoolean',
|
||||
@@ -94,9 +93,9 @@ describe('buildConfigDefinitions', () => {
|
||||
|
||||
const result = mapperFor(mockElement, t);
|
||||
|
||||
expect(t.isCallExpression(result)).toBe(true);
|
||||
expect(result.callee.property.name).toBe('numberOrBoolParser');
|
||||
expect(result.arguments[0].value).toBe('cluster');
|
||||
expect(t.isMemberExpression(result)).toBe(true);
|
||||
expect(result.object.name).toBe('parsers');
|
||||
expect(result.property.name).toBe('numberOrBooleanParser');
|
||||
});
|
||||
|
||||
it('should return numberOrStringParser call expression for NumberOrString GenericTypeAnnotation', () => {
|
||||
|
||||
@@ -6101,66 +6101,6 @@ 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', () => {
|
||||
|
||||
@@ -1,48 +1,6 @@
|
||||
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;
|
||||
|
||||
@@ -93,12 +51,8 @@ export default class MongoCollection {
|
||||
if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) {
|
||||
throw error;
|
||||
}
|
||||
// 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);
|
||||
// Figure out what key needs an index
|
||||
const key = error.message.match(/field=([A-Za-z_0-9]+) /)[1];
|
||||
if (!key) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -90,118 +90,15 @@ const IntrospectionControlPlugin = (publicIntrospection) => ({
|
||||
|
||||
});
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
// 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.
|
||||
const SchemaSuggestionsControlPlugin = (publicIntrospection) => ({
|
||||
requestDidStart: async (requestContext) => ({
|
||||
willSendResponse: async () => {
|
||||
validationDidStart: async () => {
|
||||
if (publicIntrospection) {
|
||||
return;
|
||||
}
|
||||
@@ -211,22 +108,11 @@ const SchemaSuggestionsControlPlugin = (publicIntrospection) => ({
|
||||
if (isMasterOrMaintenance) {
|
||||
return;
|
||||
}
|
||||
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)
|
||||
);
|
||||
}
|
||||
});
|
||||
return async (validationErrors) => {
|
||||
validationErrors?.forEach(error => {
|
||||
error.message = error.message.replace(/ ?Did you mean(.+?)\?$/, '');
|
||||
});
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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.numberOrBoolParser('cluster'),
|
||||
action: parsers.numberOrBooleanParser,
|
||||
},
|
||||
collectionPrefix: {
|
||||
env: 'PARSE_SERVER_COLLECTION_PREFIX',
|
||||
|
||||
@@ -1151,14 +1151,6 @@ 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;
|
||||
|
||||
+1
-9
@@ -344,9 +344,7 @@ export function getRequestQueryObject(triggerType, auth, query, count, config, c
|
||||
isGet,
|
||||
headers: config.headers,
|
||||
ip: config.ip,
|
||||
// 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 || {}),
|
||||
context: context || {},
|
||||
config,
|
||||
};
|
||||
|
||||
@@ -614,12 +612,6 @@ 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;
|
||||
|
||||
Reference in New Issue
Block a user