Skip to content

Commit 4456b02

Browse files
authored
feat: Add Parse Server option allowPublicExplain to allow Parse.Query.explain without master key (#9890)
1 parent 15c8b1a commit 4456b02

File tree

12 files changed

+180
-14
lines changed

12 files changed

+180
-14
lines changed

DEPRECATIONS.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@
22

33
The following is a list of deprecations, according to the [Deprecation Policy](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md#deprecation-policy). After a feature becomes deprecated, and giving developers time to adapt to the change, the deprecated feature will eventually be removed, leading to a breaking change. Developer feedback during the deprecation period may postpone or even revoke the introduction of the breaking change.
44

5-
| ID | Change | Issue | Deprecation [ℹ️][i_deprecation] | Planned Removal [ℹ️][i_removal] | Status [ℹ️][i_status] | Notes |
6-
|--------|-------------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------|
7-
| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
8-
| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
9-
| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
10-
| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
11-
| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
12-
| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
13-
| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
14-
| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
15-
| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | removed | - |
16-
| DEPPS10 | Encode `Parse.Object` in Cloud Function and remove option `encodeParseObjectInCloudFunction` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 9.0.0 (2026) | deprecated | - |
17-
| DEPPS11 | Replace `PublicAPIRouter` with `PagesRouter` | [#7625](https://github.com/parse-community/parse-server/issues/7625) | 8.0.0 (2025) | 9.0.0 (2026) | deprecated | - |
5+
| ID | Change | Issue | Deprecation [ℹ️][i_deprecation] | Planned Removal [ℹ️][i_removal] | Status [ℹ️][i_status] | Notes |
6+
|---------|----------------------------------------------------------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------|
7+
| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
8+
| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
9+
| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
10+
| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
11+
| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
12+
| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
13+
| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
14+
| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
15+
| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | removed | - |
16+
| DEPPS10 | Encode `Parse.Object` in Cloud Function and remove option `encodeParseObjectInCloudFunction` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 9.0.0 (2026) | deprecated | - |
17+
| DEPPS11 | Replace `PublicAPIRouter` with `PagesRouter` | [#7625](https://github.com/parse-community/parse-server/issues/7625) | 8.0.0 (2025) | 9.0.0 (2026) | deprecated | - |
18+
| DEPPS12 | Database option `allowPublicExplain` will default to `true` | [#7519](https://github.com/parse-community/parse-server/issues/7519) | 8.5.0 (2025) | 9.0.0 (2026) | deprecated | - |
1819

1920
[i_deprecation]: ## "The version and date of the deprecation."
2021
[i_removal]: ## "The version and date of the planned removal."

spec/ParseQuery.spec.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const Parse = require('parse/node');
88
const request = require('../lib/request');
99
const ParseServerRESTController = require('../lib/ParseServerRESTController').ParseServerRESTController;
1010
const ParseServer = require('../lib/ParseServer').default;
11+
const Deprecator = require('../lib/Deprecator/Deprecator').default;
1112

1213
const masterKeyHeaders = {
1314
'X-Parse-Application-Id': 'test',
@@ -5384,4 +5385,102 @@ describe('Parse.Query testing', () => {
53845385
expect(query1.length).toEqual(1);
53855386
});
53865387
});
5388+
5389+
describe('allowPublicExplain', () => {
5390+
it_id('a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d')(it_only_db('mongo'))(
5391+
'explain works with and without master key when allowPublicExplain is true',
5392+
async () => {
5393+
await reconfigureServer({
5394+
databaseAdapter: undefined,
5395+
databaseURI: 'mongodb://localhost:27017/parse',
5396+
databaseOptions: {
5397+
allowPublicExplain: true,
5398+
},
5399+
});
5400+
5401+
const obj = new TestObject({ foo: 'bar' });
5402+
await obj.save();
5403+
5404+
// Without master key
5405+
const query = new Parse.Query(TestObject);
5406+
query.explain();
5407+
const resultWithoutMasterKey = await query.find();
5408+
expect(resultWithoutMasterKey).toBeDefined();
5409+
5410+
// With master key
5411+
const queryWithMasterKey = new Parse.Query(TestObject);
5412+
queryWithMasterKey.explain();
5413+
const resultWithMasterKey = await queryWithMasterKey.find({ useMasterKey: true });
5414+
expect(resultWithMasterKey).toBeDefined();
5415+
}
5416+
);
5417+
5418+
it_id('b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e')(it_only_db('mongo'))(
5419+
'explain requires master key when allowPublicExplain is false',
5420+
async () => {
5421+
await reconfigureServer({
5422+
databaseAdapter: undefined,
5423+
databaseURI: 'mongodb://localhost:27017/parse',
5424+
databaseOptions: {
5425+
allowPublicExplain: false,
5426+
},
5427+
});
5428+
5429+
const obj = new TestObject({ foo: 'bar' });
5430+
await obj.save();
5431+
5432+
// Without master key
5433+
const query = new Parse.Query(TestObject);
5434+
query.explain();
5435+
await expectAsync(query.find()).toBeRejectedWith(
5436+
new Parse.Error(
5437+
Parse.Error.INVALID_QUERY,
5438+
'Using the explain query parameter requires the master key'
5439+
)
5440+
);
5441+
5442+
// With master key
5443+
const queryWithMasterKey = new Parse.Query(TestObject);
5444+
queryWithMasterKey.explain();
5445+
const result = await queryWithMasterKey.find({ useMasterKey: true });
5446+
expect(result).toBeDefined();
5447+
}
5448+
);
5449+
5450+
it_id('c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f')(it_only_db('mongo'))(
5451+
'explain works with and without master key by default',
5452+
async () => {
5453+
const logger = require('../lib/logger').logger;
5454+
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
5455+
5456+
await reconfigureServer({
5457+
databaseAdapter: undefined,
5458+
databaseURI: 'mongodb://localhost:27017/parse',
5459+
databaseOptions: {
5460+
allowPublicExplain: undefined,
5461+
},
5462+
});
5463+
5464+
// Verify deprecation warning is logged when allowPublicExplain is not explicitly set
5465+
expect(logSpy).toHaveBeenCalledWith(
5466+
jasmine.stringMatching(/DeprecationWarning.*databaseOptions\.allowPublicExplain.*false/)
5467+
);
5468+
5469+
const obj = new TestObject({ foo: 'bar' });
5470+
await obj.save();
5471+
5472+
// Without master key
5473+
const query = new Parse.Query(TestObject);
5474+
query.explain();
5475+
const resultWithoutMasterKey = await query.find();
5476+
expect(resultWithoutMasterKey).toBeDefined();
5477+
5478+
// With master key
5479+
const queryWithMasterKey = new Parse.Query(TestObject);
5480+
queryWithMasterKey.explain();
5481+
const resultWithMasterKey = await queryWithMasterKey.find({ useMasterKey: true });
5482+
expect(resultWithMasterKey).toBeDefined();
5483+
}
5484+
);
5485+
});
53875486
});

spec/SecurityCheckGroups.spec.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,26 @@ describe('Security Check Groups', () => {
6060
expect(group.checks()[4].checkState()).toBe(CheckState.fail);
6161
expect(group.checks()[5].checkState()).toBe(CheckState.fail);
6262
});
63+
64+
it_only_db('mongo')('checks succeed correctly (MongoDB specific)', async () => {
65+
config.databaseAdapter = undefined;
66+
config.databaseOptions = { allowPublicExplain: false };
67+
await reconfigureServer(config);
68+
69+
const group = new CheckGroupServerConfig();
70+
await group.run();
71+
expect(group.checks()[6].checkState()).toBe(CheckState.success);
72+
});
73+
74+
it_only_db('mongo')('checks fail correctly (MongoDB specific)', async () => {
75+
config.databaseAdapter = undefined;
76+
config.databaseOptions = { allowPublicExplain: true };
77+
await reconfigureServer(config);
78+
79+
const group = new CheckGroupServerConfig();
80+
await group.run();
81+
expect(group.checks()[6].checkState()).toBe(CheckState.fail);
82+
});
6383
});
6484

6585
describe('CheckGroupDatabase', () => {

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export class MongoStorageAdapter implements StorageAdapter {
148148
this._uri = uri;
149149
this._collectionPrefix = collectionPrefix;
150150
this._mongoOptions = { ...mongoOptions };
151-
this._onchange = () => { };
151+
this._onchange = () => {};
152152

153153
// MaxTimeMS is not a global MongoDB client option, it is applied per operation.
154154
this._maxTimeMS = mongoOptions.maxTimeMS;
@@ -157,10 +157,12 @@ export class MongoStorageAdapter implements StorageAdapter {
157157
this.schemaCacheTtl = mongoOptions.schemaCacheTtl;
158158
this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation;
159159
this._logClientEvents = mongoOptions.logClientEvents;
160+
160161
// Remove Parse Server-specific options that should not be passed to MongoDB client
161162
// Note: We only delete from this._mongoOptions, not from the original mongoOptions object,
162163
// because other components (like DatabaseController) need access to these options
163164
for (const key of [
165+
'allowPublicExplain',
164166
'enableSchemaHooks',
165167
'schemaCacheTtl',
166168
'maxTimeMS',

src/Config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,11 @@ export class Config {
659659
} else if (typeof databaseOptions.schemaCacheTtl !== 'number') {
660660
throw `databaseOptions.schemaCacheTtl must be a number`;
661661
}
662+
if (databaseOptions.allowPublicExplain === undefined) {
663+
databaseOptions.allowPublicExplain = DatabaseOptions.allowPublicExplain.default;
664+
} else if (typeof databaseOptions.allowPublicExplain !== 'boolean') {
665+
throw `Parse Server option 'databaseOptions.allowPublicExplain' must be a boolean.`;
666+
}
662667
}
663668

664669
static validateRateLimit(rateLimit) {

src/Deprecator/Deprecations.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@
1818
module.exports = [
1919
{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' },
2020
{ optionKey: 'enableInsecureAuthAdapters', changeNewDefault: 'false' },
21+
{ optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' },
2122
];

src/Options/Definitions.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,13 @@ module.exports.LogClientEvent = {
11371137
},
11381138
};
11391139
module.exports.DatabaseOptions = {
1140+
allowPublicExplain: {
1141+
env: 'PARSE_SERVER_DATABASE_ALLOW_PUBLIC_EXPLAIN',
1142+
help:
1143+
'Set to `true` to allow `Parse.Query.explain` without master key.<br><br>\u26A0\uFE0F Enabling this option may expose sensitive query performance data to unauthorized users and could potentially be exploited for malicious purposes.',
1144+
action: parsers.booleanParser,
1145+
default: true,
1146+
},
11401147
appName: {
11411148
env: 'PARSE_SERVER_DATABASE_APP_NAME',
11421149
help:

src/Options/docs.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Options/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,9 @@ export interface DatabaseOptions {
751751
createIndexRoleName: ?boolean;
752752
/* Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. */
753753
disableIndexFieldValidation: ?boolean;
754+
/* Set to `true` to allow `Parse.Query.explain` without master key.<br><br>⚠️ Enabling this option may expose sensitive query performance data to unauthorized users and could potentially be exploited for malicious purposes.
755+
:DEFAULT: true */
756+
allowPublicExplain: ?boolean;
754757
/* An array of MongoDB client event configurations to enable logging of specific events. */
755758
logClientEvents: ?(LogClientEvent[]);
756759
}

src/Security/CheckGroups/CheckGroupServerConfig.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,21 @@ class CheckGroupServerConfig extends CheckGroup {
9090
}
9191
},
9292
}),
93+
new Check({
94+
title: 'Public database explain disabled',
95+
warning:
96+
'Database explain queries are publicly accessible, which may expose sensitive database performance information and schema details.',
97+
solution:
98+
"Change Parse Server configuration to 'databaseOptions.allowPublicExplain: false'. You will need to use master key to run explain queries.",
99+
check: () => {
100+
if (
101+
config.databaseOptions?.allowPublicExplain === true ||
102+
config.databaseOptions?.allowPublicExplain == null
103+
) {
104+
throw 1;
105+
}
106+
},
107+
}),
93108
];
94109
}
95110
}

0 commit comments

Comments
 (0)