Skip to content

Commit 8758e6a

Browse files
feat: Prevent Parse Server start in case of unknown option in server configuration (#8987)
1 parent f1469c6 commit 8758e6a

File tree

6 files changed

+150
-4
lines changed

6 files changed

+150
-4
lines changed

resources/buildConfigDefinitions.js

+17
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,23 @@ function inject(t, list) {
254254
if (action) {
255255
props.push(t.objectProperty(t.stringLiteral('action'), action));
256256
}
257+
258+
if (t.isGenericTypeAnnotation(elt)) {
259+
if (elt.typeAnnotation.id.name in nestedOptionEnvPrefix) {
260+
props.push(
261+
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elt.typeAnnotation.id.name))
262+
);
263+
}
264+
} else if (t.isArrayTypeAnnotation(elt)) {
265+
const elementType = elt.typeAnnotation.elementType;
266+
if (t.isGenericTypeAnnotation(elementType)) {
267+
if (elementType.id.name in nestedOptionEnvPrefix) {
268+
props.push(
269+
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elementType.id.name + '[]'))
270+
);
271+
}
272+
}
273+
}
257274
if (elt.defaultValue) {
258275
let parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
259276
if (!parsedValue) {

spec/ParseConfigKey.spec.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const Config = require('../lib/Config');
2+
const ParseServer = require('../lib/index').ParseServer;
3+
4+
describe('Config Keys', () => {
5+
const tests = [
6+
{
7+
name: 'Invalid Root Keys',
8+
options: { unknow: 'val', masterKeyIPs: '' },
9+
error: 'unknow, masterKeyIPs',
10+
},
11+
{ name: 'Invalid Schema Keys', options: { schema: { Strict: 'val' } }, error: 'schema.Strict' },
12+
{
13+
name: 'Invalid Pages Keys',
14+
options: { pages: { customUrls: { EmailVerificationSendFail: 'val' } } },
15+
error: 'pages.customUrls.EmailVerificationSendFail',
16+
},
17+
{
18+
name: 'Invalid LiveQueryServerOptions Keys',
19+
options: { liveQueryServerOptions: { MasterKey: 'value' } },
20+
error: 'liveQueryServerOptions.MasterKey',
21+
},
22+
{
23+
name: 'Invalid RateLimit Keys - Array Item',
24+
options: { rateLimit: [{ RequestPath: '' }, { RequestTimeWindow: '' }] },
25+
error: 'rateLimit[0].RequestPath, rateLimit[1].RequestTimeWindow',
26+
},
27+
];
28+
29+
tests.forEach(test => {
30+
it(test.name, async () => {
31+
const logger = require('../lib/logger').logger;
32+
spyOn(logger, 'error').and.callThrough();
33+
spyOn(Config, 'validateOptions').and.callFake(() => {});
34+
35+
new ParseServer({
36+
...defaultConfiguration,
37+
...test.options,
38+
});
39+
expect(logger.error).toHaveBeenCalledWith(`Invalid Option Keys Found: ${test.error}`);
40+
});
41+
});
42+
43+
it('should run fine', async () => {
44+
try {
45+
await reconfigureServer({
46+
...defaultConfiguration,
47+
});
48+
} catch (err) {
49+
fail('Should run without error');
50+
}
51+
});
52+
});

src/Config.js

+11
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export class Config {
6464
}
6565

6666
static validateOptions({
67+
customPages,
6768
publicServerURL,
6869
revokeSessionOnPasswordReset,
6970
expireInactiveSessions,
@@ -133,9 +134,18 @@ export class Config {
133134
this.validateRateLimit(rateLimit);
134135
this.validateLogLevels(logLevels);
135136
this.validateDatabaseOptions(databaseOptions);
137+
this.validateCustomPages(customPages);
136138
this.validateAllowClientClassCreation(allowClientClassCreation);
137139
}
138140

141+
static validateCustomPages(customPages) {
142+
if (!customPages) return;
143+
144+
if (Object.prototype.toString.call(customPages) !== '[object Object]') {
145+
throw Error('Parse Server option customPages must be an object.');
146+
}
147+
}
148+
139149
static validateControllers({
140150
verifyUserEmails,
141151
userController,
@@ -569,6 +579,7 @@ export class Config {
569579
if (Object.prototype.toString.call(databaseOptions) !== '[object Object]') {
570580
throw `databaseOptions must be an object`;
571581
}
582+
572583
if (databaseOptions.enableSchemaHooks === undefined) {
573584
databaseOptions.enableSchemaHooks = DatabaseOptions.enableSchemaHooks.default;
574585
} else if (typeof databaseOptions.enableSchemaHooks !== 'boolean') {

src/Deprecator/Deprecations.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,4 @@
1515
*
1616
* If there are no deprecations, this must return an empty array.
1717
*/
18-
module.exports = [
19-
{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' },
20-
];
18+
module.exports = [{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' }];

src/Options/Definitions.js

+15
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ module.exports.ParseServerOptions = {
5454
env: 'PARSE_SERVER_ACCOUNT_LOCKOUT',
5555
help: 'The account lockout policy for failed login attempts.',
5656
action: parsers.objectParser,
57+
type: 'AccountLockoutOptions',
5758
},
5859
allowClientClassCreation: {
5960
env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION',
@@ -157,6 +158,7 @@ module.exports.ParseServerOptions = {
157158
env: 'PARSE_SERVER_CUSTOM_PAGES',
158159
help: 'custom pages for password validation and reset',
159160
action: parsers.objectParser,
161+
type: 'CustomPagesOptions',
160162
default: {},
161163
},
162164
databaseAdapter: {
@@ -169,6 +171,7 @@ module.exports.ParseServerOptions = {
169171
env: 'PARSE_SERVER_DATABASE_OPTIONS',
170172
help: 'Options to pass to the database client',
171173
action: parsers.objectParser,
174+
type: 'DatabaseOptions',
172175
},
173176
databaseURI: {
174177
env: 'PARSE_SERVER_DATABASE_URI',
@@ -273,6 +276,7 @@ module.exports.ParseServerOptions = {
273276
env: 'PARSE_SERVER_FILE_UPLOAD_OPTIONS',
274277
help: 'Options for file uploads',
275278
action: parsers.objectParser,
279+
type: 'FileUploadOptions',
276280
default: {},
277281
},
278282
graphQLPath: {
@@ -294,6 +298,7 @@ module.exports.ParseServerOptions = {
294298
help:
295299
'Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.',
296300
action: parsers.objectParser,
301+
type: 'IdempotencyOptions',
297302
default: {},
298303
},
299304
javascriptKey: {
@@ -309,11 +314,13 @@ module.exports.ParseServerOptions = {
309314
env: 'PARSE_SERVER_LIVE_QUERY',
310315
help: "parse-server's LiveQuery configuration object",
311316
action: parsers.objectParser,
317+
type: 'LiveQueryOptions',
312318
},
313319
liveQueryServerOptions: {
314320
env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS',
315321
help: 'Live query server configuration options (will start the liveQuery server)',
316322
action: parsers.objectParser,
323+
type: 'LiveQueryServerOptions',
317324
},
318325
loggerAdapter: {
319326
env: 'PARSE_SERVER_LOGGER_ADAPTER',
@@ -328,6 +335,7 @@ module.exports.ParseServerOptions = {
328335
env: 'PARSE_SERVER_LOG_LEVELS',
329336
help: '(Optional) Overrides the log levels used internally by Parse Server to log events.',
330337
action: parsers.objectParser,
338+
type: 'LogLevels',
331339
default: {},
332340
},
333341
logsFolder: {
@@ -408,12 +416,14 @@ module.exports.ParseServerOptions = {
408416
help:
409417
'The options for pages such as password reset and email verification. Caution, this is an experimental feature that may not be appropriate for production.',
410418
action: parsers.objectParser,
419+
type: 'PagesOptions',
411420
default: {},
412421
},
413422
passwordPolicy: {
414423
env: 'PARSE_SERVER_PASSWORD_POLICY',
415424
help: 'The password policy for enforcing password related rules.',
416425
action: parsers.objectParser,
426+
type: 'PasswordPolicyOptions',
417427
},
418428
playgroundPath: {
419429
env: 'PARSE_SERVER_PLAYGROUND_PATH',
@@ -471,6 +481,7 @@ module.exports.ParseServerOptions = {
471481
help:
472482
"Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.<br><br>\u2139\uFE0F Mind the following limitations:<br>- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses<br>- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable<br>- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case.",
473483
action: parsers.arrayParser,
484+
type: 'RateLimitOptions[]',
474485
default: [],
475486
},
476487
readOnlyMasterKey: {
@@ -516,11 +527,13 @@ module.exports.ParseServerOptions = {
516527
env: 'PARSE_SERVER_SCHEMA',
517528
help: 'Defined schema',
518529
action: parsers.objectParser,
530+
type: 'SchemaOptions',
519531
},
520532
security: {
521533
env: 'PARSE_SERVER_SECURITY',
522534
help: 'The security options to identify and report weak security settings.',
523535
action: parsers.objectParser,
536+
type: 'SecurityOptions',
524537
default: {},
525538
},
526539
sendUserEmailVerification: {
@@ -665,12 +678,14 @@ module.exports.PagesOptions = {
665678
env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES',
666679
help: 'The custom routes.',
667680
action: parsers.arrayParser,
681+
type: 'PagesRoute[]',
668682
default: [],
669683
},
670684
customUrls: {
671685
env: 'PARSE_SERVER_PAGES_CUSTOM_URLS',
672686
help: 'The URLs to the custom pages.',
673687
action: parsers.objectParser,
688+
type: 'PagesCustomUrlsOptions',
674689
default: {},
675690
},
676691
enableLocalization: {

src/ParseServer.js

+54-1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { SecurityRouter } from './Routers/SecurityRouter';
4545
import CheckRunner from './Security/CheckRunner';
4646
import Deprecator from './Deprecator/Deprecator';
4747
import { DefinedSchemas } from './SchemaMigrations/DefinedSchemas';
48+
import OptionsDefinitions from './Options/Definitions';
4849

4950
// Mutate the Parse object to add the Cloud Code handlers
5051
addParseCloud();
@@ -59,6 +60,58 @@ class ParseServer {
5960
constructor(options: ParseServerOptions) {
6061
// Scan for deprecated Parse Server options
6162
Deprecator.scanParseServerOptions(options);
63+
64+
const interfaces = JSON.parse(JSON.stringify(OptionsDefinitions));
65+
66+
function getValidObject(root) {
67+
const result = {};
68+
for (const key in root) {
69+
if (Object.prototype.hasOwnProperty.call(root[key], 'type')) {
70+
if (root[key].type.endsWith('[]')) {
71+
result[key] = [getValidObject(interfaces[root[key].type.slice(0, -2)])];
72+
} else {
73+
result[key] = getValidObject(interfaces[root[key].type]);
74+
}
75+
} else {
76+
result[key] = '';
77+
}
78+
}
79+
return result;
80+
}
81+
82+
const optionsBlueprint = getValidObject(interfaces['ParseServerOptions']);
83+
84+
function validateKeyNames(original, ref, name = '') {
85+
let result = [];
86+
const prefix = name + (name !== '' ? '.' : '');
87+
for (const key in original) {
88+
if (!Object.prototype.hasOwnProperty.call(ref, key)) {
89+
result.push(prefix + key);
90+
} else {
91+
if (ref[key] === '') continue;
92+
let res = [];
93+
if (Array.isArray(original[key]) && Array.isArray(ref[key])) {
94+
const type = ref[key][0];
95+
original[key].forEach((item, idx) => {
96+
if (typeof item === 'object' && item !== null) {
97+
res = res.concat(validateKeyNames(item, type, prefix + key + `[${idx}]`));
98+
}
99+
});
100+
} else if (typeof original[key] === 'object' && typeof ref[key] === 'object') {
101+
res = validateKeyNames(original[key], ref[key], prefix + key);
102+
}
103+
result = result.concat(res);
104+
}
105+
}
106+
return result;
107+
}
108+
109+
const diff = validateKeyNames(options, optionsBlueprint);
110+
if (diff.length > 0) {
111+
const logger = logging.logger;
112+
logger.error(`Invalid Option Keys Found: ${diff.join(', ')}`);
113+
}
114+
62115
// Set option defaults
63116
injectDefaults(options);
64117
const {
@@ -70,9 +123,9 @@ class ParseServer {
70123
// Initialize the node client SDK automatically
71124
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
72125
Parse.serverURL = serverURL;
73-
74126
Config.validateOptions(options);
75127
const allControllers = controllers.getControllers(options);
128+
76129
options.state = 'initialized';
77130
this.config = Config.put(Object.assign({}, options, allControllers));
78131
this.config.masterKeyIpsStore = new Map();

0 commit comments

Comments
 (0)