feat: Prevent Parse Server start in case of unknown option in server configuration (#8987)
This commit is contained in:
@@ -254,6 +254,23 @@ function inject(t, list) {
|
||||
if (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) {
|
||||
let parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
|
||||
if (!parsedValue) {
|
||||
|
||||
52
spec/ParseConfigKey.spec.js
Normal file
52
spec/ParseConfigKey.spec.js
Normal 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -64,6 +64,7 @@ export class Config {
|
||||
}
|
||||
|
||||
static validateOptions({
|
||||
customPages,
|
||||
publicServerURL,
|
||||
revokeSessionOnPasswordReset,
|
||||
expireInactiveSessions,
|
||||
@@ -133,9 +134,18 @@ export class Config {
|
||||
this.validateRateLimit(rateLimit);
|
||||
this.validateLogLevels(logLevels);
|
||||
this.validateDatabaseOptions(databaseOptions);
|
||||
this.validateCustomPages(customPages);
|
||||
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({
|
||||
verifyUserEmails,
|
||||
userController,
|
||||
@@ -569,6 +579,7 @@ export class Config {
|
||||
if (Object.prototype.toString.call(databaseOptions) !== '[object Object]') {
|
||||
throw `databaseOptions must be an object`;
|
||||
}
|
||||
|
||||
if (databaseOptions.enableSchemaHooks === undefined) {
|
||||
databaseOptions.enableSchemaHooks = DatabaseOptions.enableSchemaHooks.default;
|
||||
} else if (typeof databaseOptions.enableSchemaHooks !== 'boolean') {
|
||||
|
||||
@@ -15,6 +15,4 @@
|
||||
*
|
||||
* If there are no deprecations, this must return an empty array.
|
||||
*/
|
||||
module.exports = [
|
||||
{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' },
|
||||
];
|
||||
module.exports = [{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' }];
|
||||
|
||||
@@ -54,6 +54,7 @@ module.exports.ParseServerOptions = {
|
||||
env: 'PARSE_SERVER_ACCOUNT_LOCKOUT',
|
||||
help: 'The account lockout policy for failed login attempts.',
|
||||
action: parsers.objectParser,
|
||||
type: 'AccountLockoutOptions',
|
||||
},
|
||||
allowClientClassCreation: {
|
||||
env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION',
|
||||
@@ -157,6 +158,7 @@ module.exports.ParseServerOptions = {
|
||||
env: 'PARSE_SERVER_CUSTOM_PAGES',
|
||||
help: 'custom pages for password validation and reset',
|
||||
action: parsers.objectParser,
|
||||
type: 'CustomPagesOptions',
|
||||
default: {},
|
||||
},
|
||||
databaseAdapter: {
|
||||
@@ -169,6 +171,7 @@ module.exports.ParseServerOptions = {
|
||||
env: 'PARSE_SERVER_DATABASE_OPTIONS',
|
||||
help: 'Options to pass to the database client',
|
||||
action: parsers.objectParser,
|
||||
type: 'DatabaseOptions',
|
||||
},
|
||||
databaseURI: {
|
||||
env: 'PARSE_SERVER_DATABASE_URI',
|
||||
@@ -273,6 +276,7 @@ module.exports.ParseServerOptions = {
|
||||
env: 'PARSE_SERVER_FILE_UPLOAD_OPTIONS',
|
||||
help: 'Options for file uploads',
|
||||
action: parsers.objectParser,
|
||||
type: 'FileUploadOptions',
|
||||
default: {},
|
||||
},
|
||||
graphQLPath: {
|
||||
@@ -294,6 +298,7 @@ module.exports.ParseServerOptions = {
|
||||
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.',
|
||||
action: parsers.objectParser,
|
||||
type: 'IdempotencyOptions',
|
||||
default: {},
|
||||
},
|
||||
javascriptKey: {
|
||||
@@ -309,11 +314,13 @@ module.exports.ParseServerOptions = {
|
||||
env: 'PARSE_SERVER_LIVE_QUERY',
|
||||
help: "parse-server's LiveQuery configuration object",
|
||||
action: parsers.objectParser,
|
||||
type: 'LiveQueryOptions',
|
||||
},
|
||||
liveQueryServerOptions: {
|
||||
env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS',
|
||||
help: 'Live query server configuration options (will start the liveQuery server)',
|
||||
action: parsers.objectParser,
|
||||
type: 'LiveQueryServerOptions',
|
||||
},
|
||||
loggerAdapter: {
|
||||
env: 'PARSE_SERVER_LOGGER_ADAPTER',
|
||||
@@ -328,6 +335,7 @@ module.exports.ParseServerOptions = {
|
||||
env: 'PARSE_SERVER_LOG_LEVELS',
|
||||
help: '(Optional) Overrides the log levels used internally by Parse Server to log events.',
|
||||
action: parsers.objectParser,
|
||||
type: 'LogLevels',
|
||||
default: {},
|
||||
},
|
||||
logsFolder: {
|
||||
@@ -408,12 +416,14 @@ module.exports.ParseServerOptions = {
|
||||
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.',
|
||||
action: parsers.objectParser,
|
||||
type: 'PagesOptions',
|
||||
default: {},
|
||||
},
|
||||
passwordPolicy: {
|
||||
env: 'PARSE_SERVER_PASSWORD_POLICY',
|
||||
help: 'The password policy for enforcing password related rules.',
|
||||
action: parsers.objectParser,
|
||||
type: 'PasswordPolicyOptions',
|
||||
},
|
||||
playgroundPath: {
|
||||
env: 'PARSE_SERVER_PLAYGROUND_PATH',
|
||||
@@ -471,6 +481,7 @@ module.exports.ParseServerOptions = {
|
||||
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.",
|
||||
action: parsers.arrayParser,
|
||||
type: 'RateLimitOptions[]',
|
||||
default: [],
|
||||
},
|
||||
readOnlyMasterKey: {
|
||||
@@ -516,11 +527,13 @@ module.exports.ParseServerOptions = {
|
||||
env: 'PARSE_SERVER_SCHEMA',
|
||||
help: 'Defined schema',
|
||||
action: parsers.objectParser,
|
||||
type: 'SchemaOptions',
|
||||
},
|
||||
security: {
|
||||
env: 'PARSE_SERVER_SECURITY',
|
||||
help: 'The security options to identify and report weak security settings.',
|
||||
action: parsers.objectParser,
|
||||
type: 'SecurityOptions',
|
||||
default: {},
|
||||
},
|
||||
sendUserEmailVerification: {
|
||||
@@ -665,12 +678,14 @@ module.exports.PagesOptions = {
|
||||
env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES',
|
||||
help: 'The custom routes.',
|
||||
action: parsers.arrayParser,
|
||||
type: 'PagesRoute[]',
|
||||
default: [],
|
||||
},
|
||||
customUrls: {
|
||||
env: 'PARSE_SERVER_PAGES_CUSTOM_URLS',
|
||||
help: 'The URLs to the custom pages.',
|
||||
action: parsers.objectParser,
|
||||
type: 'PagesCustomUrlsOptions',
|
||||
default: {},
|
||||
},
|
||||
enableLocalization: {
|
||||
|
||||
@@ -45,6 +45,7 @@ import { SecurityRouter } from './Routers/SecurityRouter';
|
||||
import CheckRunner from './Security/CheckRunner';
|
||||
import Deprecator from './Deprecator/Deprecator';
|
||||
import { DefinedSchemas } from './SchemaMigrations/DefinedSchemas';
|
||||
import OptionsDefinitions from './Options/Definitions';
|
||||
|
||||
// Mutate the Parse object to add the Cloud Code handlers
|
||||
addParseCloud();
|
||||
@@ -59,6 +60,58 @@ class ParseServer {
|
||||
constructor(options: ParseServerOptions) {
|
||||
// Scan for deprecated Parse Server 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
|
||||
injectDefaults(options);
|
||||
const {
|
||||
@@ -70,9 +123,9 @@ class ParseServer {
|
||||
// Initialize the node client SDK automatically
|
||||
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
|
||||
Parse.serverURL = serverURL;
|
||||
|
||||
Config.validateOptions(options);
|
||||
const allControllers = controllers.getControllers(options);
|
||||
|
||||
options.state = 'initialized';
|
||||
this.config = Config.put(Object.assign({}, options, allControllers));
|
||||
this.config.masterKeyIpsStore = new Map();
|
||||
|
||||
Reference in New Issue
Block a user