Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ Once you have babel running in watch mode, you can start making changes to parse
* Take testing seriously! Aim to increase the test coverage with every pull request. To obtain the test coverage of the project, run: `npm run coverage`
* Run the tests for the file you are working on with the following command: `npm test spec/MyFile.spec.js`
* Run the tests for the whole project to make sure the code passes all tests. This can be done by running the test command for a single file but removing the test file argument. The results can be seen at *<PROJECT_ROOT>/coverage/lcov-report/index.html*.
* Format your code by running `npm run clean`.
* Lint your code by running `npm run lint` to make sure the code is not going to be rejected by the CI.
* **Do not** publish the *lib* folder.
* Mocks belong in the `spec/support` folder.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
},
"scripts": {
"ci:check": "node ./resources/ci/ciCheck.js",
"clean": "npm run prettier && npm run lint-fix",
"definitions": "node ./resources/buildConfigDefinitions.js && prettier --write 'src/Options/*.js'",
"docs": "jsdoc -c ./jsdoc-conf.json",
"lint": "flow && eslint --cache ./",
Expand Down
60 changes: 34 additions & 26 deletions spec/SecurityCheck.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,20 @@ describe('Security Check', () => {
await reconfigureServer(config);
}

const securityRequest = (options) => request(Object.assign({
url: securityUrl,
headers: {
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Application-Id': Parse.applicationId,
},
followRedirects: false,
}, options)).catch(e => e);
const securityRequest = options =>
request(
Object.assign(
{
url: securityUrl,
headers: {
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Application-Id': Parse.applicationId,
},
followRedirects: false,
},
options
)
).catch(e => e);

beforeEach(async () => {
groupName = 'Example Group Name';
Expand All @@ -41,7 +47,7 @@ describe('Security Check', () => {
solution: 'TestSolution',
check: () => {
return true;
}
},
});
checkFail = new Check({
group: 'TestGroup',
Expand All @@ -50,14 +56,14 @@ describe('Security Check', () => {
solution: 'TestSolution',
check: () => {
throw 'Fail';
}
},
});
Group = class Group extends CheckGroup {
setName() {
return groupName;
}
setChecks() {
return [ checkSuccess, checkFail ];
return [checkSuccess, checkFail];
}
};
config = {
Expand Down Expand Up @@ -154,7 +160,7 @@ describe('Security Check', () => {
title: 'string',
warning: 'string',
solution: 'string',
check: () => {}
check: () => {},
},
{
group: 'string',
Expand Down Expand Up @@ -203,7 +209,9 @@ describe('Security Check', () => {
title: 'string',
warning: 'string',
solution: 'string',
check: () => { throw 'error' },
check: () => {
throw 'error';
},
});
expect(check._checkState == CheckState.none);
check.run();
Expand Down Expand Up @@ -277,7 +285,7 @@ describe('Security Check', () => {
});

it('runs all checks of all groups', async () => {
const checkGroups = [ Group, Group ];
const checkGroups = [Group, Group];
const runner = new CheckRunner({ checkGroups });
const report = await runner.run();
expect(report.report.groups[0].checks[0].state).toBe(CheckState.success);
Expand All @@ -287,27 +295,27 @@ describe('Security Check', () => {
});

it('reports correct default syntax version 1.0.0', async () => {
const checkGroups = [ Group ];
const checkGroups = [Group];
const runner = new CheckRunner({ checkGroups, enableCheckLog: true });
const report = await runner.run();
expect(report).toEqual({
report: {
version: "1.0.0",
state: "fail",
version: '1.0.0',
state: 'fail',
groups: [
{
name: "Example Group Name",
state: "fail",
name: 'Example Group Name',
state: 'fail',
checks: [
{
title: "TestTitleSuccess",
state: "success",
title: 'TestTitleSuccess',
state: 'success',
},
{
title: "TestTitleFail",
state: "fail",
warning: "TestWarning",
solution: "TestSolution",
title: 'TestTitleFail',
state: 'fail',
warning: 'TestWarning',
solution: 'TestSolution',
},
],
},
Expand All @@ -319,7 +327,7 @@ describe('Security Check', () => {
it('logs report', async () => {
const logger = require('../lib/logger').logger;
const logSpy = spyOn(logger, 'warn').and.callThrough();
const checkGroups = [ Group ];
const checkGroups = [Group];
const runner = new CheckRunner({ checkGroups, enableCheckLog: true });
const report = await runner.run();
const titles = report.report.groups.flatMap(group => group.checks.map(check => check.title));
Expand Down
6 changes: 4 additions & 2 deletions src/Routers/SecurityRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import CheckRunner from '../Security/CheckRunner';

export class SecurityRouter extends PromiseRouter {
mountRoutes() {
this.route('GET', '/security',
this.route(
'GET',
'/security',
middleware.promiseEnforceMasterKeyAccess,
this._enforceSecurityCheckEnabled,
async (req) => {
async req => {
const report = await new CheckRunner(req.config.security).run();
return {
status: 200,
Expand Down
6 changes: 3 additions & 3 deletions src/Security/Check.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ class Check {
* The check state.
*/
const CheckState = Object.freeze({
none: "none",
fail: "fail",
success: "success",
none: 'none',
fail: 'fail',
success: 'success',
});

export default Check;
Expand Down
9 changes: 5 additions & 4 deletions src/Security/CheckGroups/CheckGroupDatabase.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ 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.
*/
* The security checks group for Parse Server configuration.
* Checks common Parse Server parameters such as access keys.
*/
class CheckGroupDatabase extends CheckGroup {
setName() {
return 'Database';
Expand All @@ -23,7 +23,8 @@ class CheckGroupDatabase extends CheckGroup {
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.',
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);
Expand Down
16 changes: 9 additions & 7 deletions src/Security/CheckGroups/CheckGroupServerConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ 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.
*/
* 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';
Expand All @@ -21,7 +21,8 @@ class CheckGroupServerConfig extends CheckGroup {
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.',
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);
Expand All @@ -41,7 +42,7 @@ class CheckGroupServerConfig extends CheckGroup {
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\'.',
solution: "Change Parse Server configuration to 'security.enableCheckLog: false'.",
check: () => {
if (config.security && config.security.enableCheckLog) {
throw 1;
Expand All @@ -50,8 +51,9 @@ class CheckGroupServerConfig extends CheckGroup {
}),
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\'.',
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;
Expand Down
28 changes: 16 additions & 12 deletions src/Security/CheckRunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class CheckRunner {

// If report should be written to logs
if (this.enableCheckLog) {
this._logReport(report)
this._logReport(report);
}
return report;
}
Expand Down Expand Up @@ -85,8 +85,8 @@ class CheckRunner {
report: {
version,
state: CheckState.success,
groups: []
}
groups: [],
},
};

// Identify report version
Expand All @@ -95,13 +95,12 @@ class CheckRunner {
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 => {
Expand Down Expand Up @@ -129,9 +128,9 @@ class CheckRunner {
* @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);
const log =
report.report.state == CheckState.success ? s => logger.info(s) : s => logger.warn(s);

// Declare output
const indent = ' ';
Expand All @@ -142,7 +141,7 @@ class CheckRunner {

// Traverse all groups and checks for compose output
for (const group of report.report.groups) {
output += `\n- ${group.name}`
output += `\n- ${group.name}`;

for (const check of group.checks) {
checksCount++;
Expand All @@ -166,7 +165,9 @@ class CheckRunner {
`\n# #` +
`\n###################################` +
`\n` +
`\n${failedChecksCount > 0 ? 'Warning: ' : ''}${failedChecksCount} weak security setting(s) found${failedChecksCount > 0 ? '!' : ''}` +
`\n${
failedChecksCount > 0 ? 'Warning: ' : ''
}${failedChecksCount} weak security setting(s) found${failedChecksCount > 0 ? '!' : ''}` +
`\n${checksCount} check(s) executed` +
`\n${skippedCheckCount} check(s) skipped` +
`\n` +
Expand All @@ -183,9 +184,12 @@ class CheckRunner {
*/
_getLogIconForState(state) {
switch (state) {
case CheckState.success: return '✅';
case CheckState.fail: return '❌';
default: return 'ℹ️';
case CheckState.success:
return '✅';
case CheckState.fail:
return '❌';
default:
return 'ℹ️';
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ class Utils {
const type = types[key];
const isOptional = !!type.o;
const param = params[key];
if (!(isOptional && param == null) && (!type.v(param))) {
if (!(isOptional && param == null) && !type.v(param)) {
throw `Invalid parameter ${key} must be of type ${type.t} but is ${typeof param}`;
}
}
Expand Down