Add security check (#7247)

* added Parse Server security option

* added SecurityRouter

* added Check class

* added CheckGroup class

* moved parameter validation to Utils

* added CheckRunner class

* added auto-run on server start

* added custom security checks as Parse Server option

* renamed script to check

* reformat log output

* added server config check

* improved contributing guideline

* improved contribution guide

* added check security log

* improved log format

* added checks

* fixed log fomat typo

* added database checks

* fixed database check

* removed database auth check in initial version

* improved contribution guide

* added security check tests

* fixed typo

* improved wording guidelines

* improved wording guidelines
This commit is contained in:
Manuel
2021-03-10 20:19:28 +01:00
committed by GitHub
parent 36c2608400
commit bee889a329
17 changed files with 1096 additions and 2 deletions

85
src/Security/Check.js Normal file
View File

@@ -0,0 +1,85 @@
/**
* @module SecurityCheck
*/
import Utils from '../Utils';
import { isFunction, isString } from 'lodash';
/**
* A security check.
* @class Check
*/
class Check {
/**
* Constructs a new security check.
* @param {Object} params The parameters.
* @param {String} params.title The title.
* @param {String} params.warning The warning message if the check fails.
* @param {String} params.solution The solution to fix the check.
* @param {Promise} params.check The check as synchronous or asynchronous function.
*/
constructor(params) {
this._validateParams(params);
const { title, warning, solution, check } = params;
this.title = title;
this.warning = warning;
this.solution = solution;
this.check = check;
// Set default properties
this._checkState = CheckState.none;
this.error;
}
/**
* Returns the current check state.
* @return {CheckState} The check state.
*/
checkState() {
return this._checkState;
}
async run() {
// Get check as synchronous or asynchronous function
const check = this.check instanceof Promise ? await this.check : this.check;
// Run check
try {
check();
this._checkState = CheckState.success;
} catch (e) {
this.stateFailError = e;
this._checkState = CheckState.fail;
}
}
/**
* Validates the constructor parameters.
* @param {Object} params The parameters to validate.
*/
_validateParams(params) {
Utils.validateParams(params, {
group: { t: 'string', v: isString },
title: { t: 'string', v: isString },
warning: { t: 'string', v: isString },
solution: { t: 'string', v: isString },
check: { t: 'function', v: isFunction },
});
}
}
/**
* The check state.
*/
const CheckState = Object.freeze({
none: "none",
fail: "fail",
success: "success",
});
export default Check;
module.exports = {
Check,
CheckState,
};

View File

@@ -0,0 +1,45 @@
/**
* @module SecurityCheck
*/
/**
* A group of security checks.
* @interface CheckGroup
*/
class CheckGroup {
constructor() {
this._name = this.setName();
this._checks = this.setChecks();
}
/**
* The security check group name; to be overridden by child class.
*/
setName() {
throw `Check group has no name.`;
}
name() {
return this._name;
}
/**
* The security checks; to be overridden by child class.
*/
setChecks() {
throw `Check group has no checks.`;
}
checks() {
return this._checks;
}
/**
* Runs all checks.
*/
async run() {
for (const check of this._checks) {
check.run();
}
}
}
module.exports = CheckGroup;

View File

@@ -0,0 +1,47 @@
/**
* @module SecurityCheck
*/
import { Check } from '../Check';
import CheckGroup from '../CheckGroup';
import Config from '../../Config';
import Parse from 'parse/node';
/**
* The security checks group for Parse Server configuration.
* Checks common Parse Server parameters such as access keys.
*/
class CheckGroupDatabase extends CheckGroup {
setName() {
return 'Database';
}
setChecks() {
const config = Config.get(Parse.applicationId);
const databaseAdapter = config.database.adapter;
const databaseUrl = databaseAdapter._uri;
return [
new Check({
title: 'Secure database password',
warning: 'The database password is insecure and vulnerable to brute force attacks.',
solution: 'Choose a longer and/or more complex password with a combination of upper- and lowercase characters, numbers and special characters.',
check: () => {
const password = databaseUrl.match(/\/\/\S+:(\S+)@/)[1];
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasNonAlphasNumerics = /\W/.test(password);
// Ensure length
if (password.length < 14) {
throw 1;
}
// Ensure at least 3 out of 4 requirements passed
if (hasUpperCase + hasLowerCase + hasNumbers + hasNonAlphasNumerics < 3) {
throw 1;
}
},
}),
];
}
}
module.exports = CheckGroupDatabase;

View File

@@ -0,0 +1,65 @@
/**
* @module SecurityCheck
*/
import { Check } from '../Check';
import CheckGroup from '../CheckGroup';
import Config from '../../Config';
import Parse from 'parse/node';
/**
* The security checks group for Parse Server configuration.
* Checks common Parse Server parameters such as access keys.
*/
class CheckGroupServerConfig extends CheckGroup {
setName() {
return 'Parse Server Configuration';
}
setChecks() {
const config = Config.get(Parse.applicationId);
return [
new Check({
title: 'Secure master key',
warning: 'The Parse Server master key is insecure and vulnerable to brute force attacks.',
solution: 'Choose a longer and/or more complex master key with a combination of upper- and lowercase characters, numbers and special characters.',
check: () => {
const masterKey = config.masterKey;
const hasUpperCase = /[A-Z]/.test(masterKey);
const hasLowerCase = /[a-z]/.test(masterKey);
const hasNumbers = /\d/.test(masterKey);
const hasNonAlphasNumerics = /\W/.test(masterKey);
// Ensure length
if (masterKey.length < 14) {
throw 1;
}
// Ensure at least 3 out of 4 requirements passed
if (hasUpperCase + hasLowerCase + hasNumbers + hasNonAlphasNumerics < 3) {
throw 1;
}
},
}),
new Check({
title: 'Security log disabled',
warning: 'Security checks in logs may expose vulnerabilities to anyone access to logs.',
solution: 'Change Parse Server configuration to \'security.enableCheckLog: false\'.',
check: () => {
if (config.security && config.security.enableCheckLog) {
throw 1;
}
},
}),
new Check({
title: 'Client class creation disabled',
warning: 'Attackers are allowed to create new classes without restriction and flood the database.',
solution: 'Change Parse Server configuration to \'allowClientClassCreation: false\'.',
check: () => {
if (config.allowClientClassCreation || config.allowClientClassCreation == null) {
throw 1;
}
},
}),
];
}
}
module.exports = CheckGroupServerConfig;

View File

@@ -0,0 +1,9 @@
/**
* @module SecurityCheck
*/
/**
* The list of security check groups.
*/
export { default as CheckGroupDatabase } from './CheckGroupDatabase';
export { default as CheckGroupServerConfig } from './CheckGroupServerConfig';

205
src/Security/CheckRunner.js Normal file
View File

@@ -0,0 +1,205 @@
/**
* @module SecurityCheck
*/
import Utils from '../Utils';
import { CheckState } from './Check';
import * as CheckGroups from './CheckGroups/CheckGroups';
import logger from '../logger';
import { isArray, isBoolean } from 'lodash';
/**
* The security check runner.
*/
class CheckRunner {
/**
* The security check runner.
* @param {Object} [config] The configuration options.
* @param {Boolean} [config.enableCheck=false] Is true if Parse Server should report weak security settings.
* @param {Boolean} [config.enableCheckLog=false] Is true if the security check report should be written to logs.
* @param {Object} [config.checkGroups] The check groups to run. Default are the groups defined in `./CheckGroups/CheckGroups.js`.
*/
constructor(config = {}) {
this._validateParams(config);
const { enableCheck = false, enableCheckLog = false, checkGroups = CheckGroups } = config;
this.enableCheck = enableCheck;
this.enableCheckLog = enableCheckLog;
this.checkGroups = checkGroups;
}
/**
* Runs all security checks and returns the results.
* @params
* @returns {Object} The security check report.
*/
async run({ version = '1.0.0' } = {}) {
// Instantiate check groups
const groups = Object.values(this.checkGroups)
.filter(c => typeof c === 'function')
.map(CheckGroup => new CheckGroup());
// Run checks
groups.forEach(group => group.run());
// Generate JSON report
const report = this._generateReport({ groups, version });
// If report should be written to logs
if (this.enableCheckLog) {
this._logReport(report)
}
return report;
}
/**
* Generates a security check report in JSON format with schema:
* ```
* {
* report: {
* version: "1.0.0", // The report version, defines the schema
* state: "fail" // The disjunctive indicator of failed checks in all groups.
* groups: [ // The check groups
* {
* name: "House", // The group name
* state: "fail" // The disjunctive indicator of failed checks in this group.
* checks: [ // The checks
* title: "Door locked", // The check title
* state: "fail" // The check state
* warning: "Anyone can enter your house." // The warning.
* solution: "Lock your door." // The solution.
* ]
* },
* ...
* ]
* }
* }
* ```
* @param {Object} params The parameters.
* @param {Array<CheckGroup>} params.groups The check groups.
* @param {String} params.version: The report schema version.
* @returns {Object} The report.
*/
_generateReport({ groups, version }) {
// Create report template
const report = {
report: {
version,
state: CheckState.success,
groups: []
}
};
// Identify report version
switch (version) {
case '1.0.0':
default:
// For each check group
for (const group of groups) {
// Create group report
const groupReport = {
name: group.name(),
state: CheckState.success,
checks: [],
}
// Create check reports
groupReport.checks = group.checks().map(check => {
const checkReport = {
title: check.title,
state: check.checkState(),
};
if (check.checkState() == CheckState.fail) {
checkReport.warning = check.warning;
checkReport.solution = check.solution;
report.report.state = CheckState.fail;
groupReport.state = CheckState.fail;
}
return checkReport;
});
report.report.groups.push(groupReport);
}
}
return report;
}
/**
* Logs the security check report.
* @param {Object} report The report to log.
*/
_logReport(report) {
// Determine log level depending on whether any check failed
const log = report.report.state == CheckState.success ? (s) => logger.info(s) : (s) => logger.warn(s);
// Declare output
const indent = ' ';
let output = '';
let checksCount = 0;
let failedChecksCount = 0;
let skippedCheckCount = 0;
// Traverse all groups and checks for compose output
for (const group of report.report.groups) {
output += `\n- ${group.name}`
for (const check of group.checks) {
checksCount++;
output += `\n${indent}${this._getLogIconForState(check.state)} ${check.title}`;
if (check.state == CheckState.fail) {
failedChecksCount++;
output += `\n${indent}${indent}Warning: ${check.warning}`;
output += ` ${check.solution}`;
} else if (check.state == CheckState.none) {
skippedCheckCount++;
output += `\n${indent}${indent}Test did not execute, this is likely an internal server issue, please report.`;
}
}
}
output =
`\n###################################` +
`\n# #` +
`\n# Parse Server Security Check #` +
`\n# #` +
`\n###################################` +
`\n` +
`\n${failedChecksCount > 0 ? 'Warning: ' : ''}${failedChecksCount} weak security setting(s) found${failedChecksCount > 0 ? '!' : ''}` +
`\n${checksCount} check(s) executed` +
`\n${skippedCheckCount} check(s) skipped` +
`\n` +
`${output}`;
// Write log
log(output);
}
/**
* Returns an icon for use in the report log output.
* @param {CheckState} state The check state.
* @returns {String} The icon.
*/
_getLogIconForState(state) {
switch (state) {
case CheckState.success: return '✅';
case CheckState.fail: return '❌';
default: return '';
}
}
/**
* Validates the constructor parameters.
* @param {Object} params The parameters to validate.
*/
_validateParams(params) {
Utils.validateParams(params, {
enableCheck: { t: 'boolean', v: isBoolean, o: true },
enableCheckLog: { t: 'boolean', v: isBoolean, o: true },
checkGroups: { t: 'array', v: isArray, o: true },
});
}
}
module.exports = CheckRunner;