feat: Prevent Parse Server start in case of unknown option in server configuration (#8987)

This commit is contained in:
Vivek Joshi
2024-04-07 18:58:15 +05:30
committed by GitHub
parent f1469c6425
commit 8758e6abb9
6 changed files with 150 additions and 4 deletions

View File

@@ -254,6 +254,23 @@ function inject(t, list) {
if (action) { if (action) {
props.push(t.objectProperty(t.stringLiteral('action'), action)); props.push(t.objectProperty(t.stringLiteral('action'), action));
} }
if (t.isGenericTypeAnnotation(elt)) {
if (elt.typeAnnotation.id.name in nestedOptionEnvPrefix) {
props.push(
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elt.typeAnnotation.id.name))
);
}
} else if (t.isArrayTypeAnnotation(elt)) {
const elementType = elt.typeAnnotation.elementType;
if (t.isGenericTypeAnnotation(elementType)) {
if (elementType.id.name in nestedOptionEnvPrefix) {
props.push(
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elementType.id.name + '[]'))
);
}
}
}
if (elt.defaultValue) { if (elt.defaultValue) {
let parsedValue = parseDefaultValue(elt, elt.defaultValue, t); let parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
if (!parsedValue) { if (!parsedValue) {

View File

@@ -0,0 +1,52 @@
const Config = require('../lib/Config');
const ParseServer = require('../lib/index').ParseServer;
describe('Config Keys', () => {
const tests = [
{
name: 'Invalid Root Keys',
options: { unknow: 'val', masterKeyIPs: '' },
error: 'unknow, masterKeyIPs',
},
{ name: 'Invalid Schema Keys', options: { schema: { Strict: 'val' } }, error: 'schema.Strict' },
{
name: 'Invalid Pages Keys',
options: { pages: { customUrls: { EmailVerificationSendFail: 'val' } } },
error: 'pages.customUrls.EmailVerificationSendFail',
},
{
name: 'Invalid LiveQueryServerOptions Keys',
options: { liveQueryServerOptions: { MasterKey: 'value' } },
error: 'liveQueryServerOptions.MasterKey',
},
{
name: 'Invalid RateLimit Keys - Array Item',
options: { rateLimit: [{ RequestPath: '' }, { RequestTimeWindow: '' }] },
error: 'rateLimit[0].RequestPath, rateLimit[1].RequestTimeWindow',
},
];
tests.forEach(test => {
it(test.name, async () => {
const logger = require('../lib/logger').logger;
spyOn(logger, 'error').and.callThrough();
spyOn(Config, 'validateOptions').and.callFake(() => {});
new ParseServer({
...defaultConfiguration,
...test.options,
});
expect(logger.error).toHaveBeenCalledWith(`Invalid Option Keys Found: ${test.error}`);
});
});
it('should run fine', async () => {
try {
await reconfigureServer({
...defaultConfiguration,
});
} catch (err) {
fail('Should run without error');
}
});
});

View File

@@ -64,6 +64,7 @@ export class Config {
} }
static validateOptions({ static validateOptions({
customPages,
publicServerURL, publicServerURL,
revokeSessionOnPasswordReset, revokeSessionOnPasswordReset,
expireInactiveSessions, expireInactiveSessions,
@@ -133,9 +134,18 @@ export class Config {
this.validateRateLimit(rateLimit); this.validateRateLimit(rateLimit);
this.validateLogLevels(logLevels); this.validateLogLevels(logLevels);
this.validateDatabaseOptions(databaseOptions); this.validateDatabaseOptions(databaseOptions);
this.validateCustomPages(customPages);
this.validateAllowClientClassCreation(allowClientClassCreation); this.validateAllowClientClassCreation(allowClientClassCreation);
} }
static validateCustomPages(customPages) {
if (!customPages) return;
if (Object.prototype.toString.call(customPages) !== '[object Object]') {
throw Error('Parse Server option customPages must be an object.');
}
}
static validateControllers({ static validateControllers({
verifyUserEmails, verifyUserEmails,
userController, userController,
@@ -569,6 +579,7 @@ export class Config {
if (Object.prototype.toString.call(databaseOptions) !== '[object Object]') { if (Object.prototype.toString.call(databaseOptions) !== '[object Object]') {
throw `databaseOptions must be an object`; throw `databaseOptions must be an object`;
} }
if (databaseOptions.enableSchemaHooks === undefined) { if (databaseOptions.enableSchemaHooks === undefined) {
databaseOptions.enableSchemaHooks = DatabaseOptions.enableSchemaHooks.default; databaseOptions.enableSchemaHooks = DatabaseOptions.enableSchemaHooks.default;
} else if (typeof databaseOptions.enableSchemaHooks !== 'boolean') { } else if (typeof databaseOptions.enableSchemaHooks !== 'boolean') {

View File

@@ -15,6 +15,4 @@
* *
* If there are no deprecations, this must return an empty array. * If there are no deprecations, this must return an empty array.
*/ */
module.exports = [ module.exports = [{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' }];
{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' },
];

View File

@@ -54,6 +54,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_ACCOUNT_LOCKOUT', env: 'PARSE_SERVER_ACCOUNT_LOCKOUT',
help: 'The account lockout policy for failed login attempts.', help: 'The account lockout policy for failed login attempts.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'AccountLockoutOptions',
}, },
allowClientClassCreation: { allowClientClassCreation: {
env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION', env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION',
@@ -157,6 +158,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_CUSTOM_PAGES', env: 'PARSE_SERVER_CUSTOM_PAGES',
help: 'custom pages for password validation and reset', help: 'custom pages for password validation and reset',
action: parsers.objectParser, action: parsers.objectParser,
type: 'CustomPagesOptions',
default: {}, default: {},
}, },
databaseAdapter: { databaseAdapter: {
@@ -169,6 +171,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_DATABASE_OPTIONS', env: 'PARSE_SERVER_DATABASE_OPTIONS',
help: 'Options to pass to the database client', help: 'Options to pass to the database client',
action: parsers.objectParser, action: parsers.objectParser,
type: 'DatabaseOptions',
}, },
databaseURI: { databaseURI: {
env: 'PARSE_SERVER_DATABASE_URI', env: 'PARSE_SERVER_DATABASE_URI',
@@ -273,6 +276,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_FILE_UPLOAD_OPTIONS', env: 'PARSE_SERVER_FILE_UPLOAD_OPTIONS',
help: 'Options for file uploads', help: 'Options for file uploads',
action: parsers.objectParser, action: parsers.objectParser,
type: 'FileUploadOptions',
default: {}, default: {},
}, },
graphQLPath: { graphQLPath: {
@@ -294,6 +298,7 @@ module.exports.ParseServerOptions = {
help: help:
'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.', '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.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'IdempotencyOptions',
default: {}, default: {},
}, },
javascriptKey: { javascriptKey: {
@@ -309,11 +314,13 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_LIVE_QUERY', env: 'PARSE_SERVER_LIVE_QUERY',
help: "parse-server's LiveQuery configuration object", help: "parse-server's LiveQuery configuration object",
action: parsers.objectParser, action: parsers.objectParser,
type: 'LiveQueryOptions',
}, },
liveQueryServerOptions: { liveQueryServerOptions: {
env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS', env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS',
help: 'Live query server configuration options (will start the liveQuery server)', help: 'Live query server configuration options (will start the liveQuery server)',
action: parsers.objectParser, action: parsers.objectParser,
type: 'LiveQueryServerOptions',
}, },
loggerAdapter: { loggerAdapter: {
env: 'PARSE_SERVER_LOGGER_ADAPTER', env: 'PARSE_SERVER_LOGGER_ADAPTER',
@@ -328,6 +335,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_LOG_LEVELS', env: 'PARSE_SERVER_LOG_LEVELS',
help: '(Optional) Overrides the log levels used internally by Parse Server to log events.', help: '(Optional) Overrides the log levels used internally by Parse Server to log events.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'LogLevels',
default: {}, default: {},
}, },
logsFolder: { logsFolder: {
@@ -408,12 +416,14 @@ module.exports.ParseServerOptions = {
help: help:
'The options for pages such as password reset and email verification. Caution, this is an experimental feature that may not be appropriate for production.', 'The options for pages such as password reset and email verification. Caution, this is an experimental feature that may not be appropriate for production.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'PagesOptions',
default: {}, default: {},
}, },
passwordPolicy: { passwordPolicy: {
env: 'PARSE_SERVER_PASSWORD_POLICY', env: 'PARSE_SERVER_PASSWORD_POLICY',
help: 'The password policy for enforcing password related rules.', help: 'The password policy for enforcing password related rules.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'PasswordPolicyOptions',
}, },
playgroundPath: { playgroundPath: {
env: 'PARSE_SERVER_PLAYGROUND_PATH', env: 'PARSE_SERVER_PLAYGROUND_PATH',
@@ -471,6 +481,7 @@ module.exports.ParseServerOptions = {
help: help:
"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.", "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.",
action: parsers.arrayParser, action: parsers.arrayParser,
type: 'RateLimitOptions[]',
default: [], default: [],
}, },
readOnlyMasterKey: { readOnlyMasterKey: {
@@ -516,11 +527,13 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_SCHEMA', env: 'PARSE_SERVER_SCHEMA',
help: 'Defined schema', help: 'Defined schema',
action: parsers.objectParser, action: parsers.objectParser,
type: 'SchemaOptions',
}, },
security: { security: {
env: 'PARSE_SERVER_SECURITY', env: 'PARSE_SERVER_SECURITY',
help: 'The security options to identify and report weak security settings.', help: 'The security options to identify and report weak security settings.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'SecurityOptions',
default: {}, default: {},
}, },
sendUserEmailVerification: { sendUserEmailVerification: {
@@ -665,12 +678,14 @@ module.exports.PagesOptions = {
env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES', env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES',
help: 'The custom routes.', help: 'The custom routes.',
action: parsers.arrayParser, action: parsers.arrayParser,
type: 'PagesRoute[]',
default: [], default: [],
}, },
customUrls: { customUrls: {
env: 'PARSE_SERVER_PAGES_CUSTOM_URLS', env: 'PARSE_SERVER_PAGES_CUSTOM_URLS',
help: 'The URLs to the custom pages.', help: 'The URLs to the custom pages.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'PagesCustomUrlsOptions',
default: {}, default: {},
}, },
enableLocalization: { enableLocalization: {

View File

@@ -45,6 +45,7 @@ import { SecurityRouter } from './Routers/SecurityRouter';
import CheckRunner from './Security/CheckRunner'; import CheckRunner from './Security/CheckRunner';
import Deprecator from './Deprecator/Deprecator'; import Deprecator from './Deprecator/Deprecator';
import { DefinedSchemas } from './SchemaMigrations/DefinedSchemas'; import { DefinedSchemas } from './SchemaMigrations/DefinedSchemas';
import OptionsDefinitions from './Options/Definitions';
// Mutate the Parse object to add the Cloud Code handlers // Mutate the Parse object to add the Cloud Code handlers
addParseCloud(); addParseCloud();
@@ -59,6 +60,58 @@ class ParseServer {
constructor(options: ParseServerOptions) { constructor(options: ParseServerOptions) {
// Scan for deprecated Parse Server options // Scan for deprecated Parse Server options
Deprecator.scanParseServerOptions(options); Deprecator.scanParseServerOptions(options);
const interfaces = JSON.parse(JSON.stringify(OptionsDefinitions));
function getValidObject(root) {
const result = {};
for (const key in root) {
if (Object.prototype.hasOwnProperty.call(root[key], 'type')) {
if (root[key].type.endsWith('[]')) {
result[key] = [getValidObject(interfaces[root[key].type.slice(0, -2)])];
} else {
result[key] = getValidObject(interfaces[root[key].type]);
}
} else {
result[key] = '';
}
}
return result;
}
const optionsBlueprint = getValidObject(interfaces['ParseServerOptions']);
function validateKeyNames(original, ref, name = '') {
let result = [];
const prefix = name + (name !== '' ? '.' : '');
for (const key in original) {
if (!Object.prototype.hasOwnProperty.call(ref, key)) {
result.push(prefix + key);
} else {
if (ref[key] === '') continue;
let res = [];
if (Array.isArray(original[key]) && Array.isArray(ref[key])) {
const type = ref[key][0];
original[key].forEach((item, idx) => {
if (typeof item === 'object' && item !== null) {
res = res.concat(validateKeyNames(item, type, prefix + key + `[${idx}]`));
}
});
} else if (typeof original[key] === 'object' && typeof ref[key] === 'object') {
res = validateKeyNames(original[key], ref[key], prefix + key);
}
result = result.concat(res);
}
}
return result;
}
const diff = validateKeyNames(options, optionsBlueprint);
if (diff.length > 0) {
const logger = logging.logger;
logger.error(`Invalid Option Keys Found: ${diff.join(', ')}`);
}
// Set option defaults // Set option defaults
injectDefaults(options); injectDefaults(options);
const { const {
@@ -70,9 +123,9 @@ class ParseServer {
// Initialize the node client SDK automatically // Initialize the node client SDK automatically
Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.initialize(appId, javascriptKey || 'unused', masterKey);
Parse.serverURL = serverURL; Parse.serverURL = serverURL;
Config.validateOptions(options); Config.validateOptions(options);
const allControllers = controllers.getControllers(options); const allControllers = controllers.getControllers(options);
options.state = 'initialized'; options.state = 'initialized';
this.config = Config.put(Object.assign({}, options, allControllers)); this.config = Config.put(Object.assign({}, options, allControllers));
this.config.masterKeyIpsStore = new Map(); this.config.masterKeyIpsStore = new Map();