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:
85
src/Security/Check.js
Normal file
85
src/Security/Check.js
Normal 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,
|
||||
};
|
||||
45
src/Security/CheckGroup.js
Normal file
45
src/Security/CheckGroup.js
Normal 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;
|
||||
47
src/Security/CheckGroups/CheckGroupDatabase.js
Normal file
47
src/Security/CheckGroups/CheckGroupDatabase.js
Normal 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;
|
||||
65
src/Security/CheckGroups/CheckGroupServerConfig.js
Normal file
65
src/Security/CheckGroups/CheckGroupServerConfig.js
Normal 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;
|
||||
9
src/Security/CheckGroups/CheckGroups.js
Normal file
9
src/Security/CheckGroups/CheckGroups.js
Normal 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
205
src/Security/CheckRunner.js
Normal 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;
|
||||
Reference in New Issue
Block a user