Skip to content

Add Security Checks Log #6973

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 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
13 changes: 4 additions & 9 deletions resources/buildConfigDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ function getENVPrefix(iface) {
'LiveQueryOptions' : 'PARSE_SERVER_LIVEQUERY_',
'IdempotencyOptions' : 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
'AccountLockoutOptions' : 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_'
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_',
'SecurityChecksOptions' : 'PARSE_SERVER_SECURITY_CHECKS_'
}
if (options[iface.id.name]) {
return options[iface.id.name]
Expand Down Expand Up @@ -163,14 +164,8 @@ function parseDefaultValue(elt, value, t) {
if (type == 'NumberOrBoolean') {
literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
}
if (type == 'CustomPagesOptions') {
const object = parsers.objectParser(value);
const props = Object.keys(object).map((key) => {
return t.objectProperty(key, object[value]);
});
literalValue = t.objectExpression(props);
}
if (type == 'IdempotencyOptions') {
const literalTypes = ['IdempotencyOptions','CustomPagesOptions','SecurityChecksOptions'];
if (literalTypes.includes(type)) {
const object = parsers.objectParser(value);
const props = Object.keys(object).map((key) => {
return t.objectProperty(key, object[value]);
Expand Down
75 changes: 75 additions & 0 deletions spec/ParseServer.Security.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict';
const Parse = require('parse/node');
const request = require('../lib/request');

const defaultHeaders = {
'X-Parse-Application-Id': 'test',
'X-Parse-Rest-API-Key': 'rest',
'Content-Type': 'application/json',
};
const masterKeyHeaders = {
'X-Parse-Application-Id': 'test',
'X-Parse-Rest-API-Key': 'rest',
'X-Parse-Master-Key': 'test',
'Content-Type': 'application/json',
};
const defaultOptions = {
headers: defaultHeaders,
json: true,
};
const masterKeyOptions = {
headers: masterKeyHeaders,
json: true,
};

describe('SecurityChecks', () => {
it('should reject access when not using masterKey (/securityChecks)', done => {
request(
Object.assign({ url: Parse.serverURL + '/securityChecks' }, defaultOptions)
).then(done.fail, () => done());
});
it('should reject access by default to /securityChecks, even with masterKey', done => {
request(
Object.assign({ url: Parse.serverURL + '/securityChecks' }, masterKeyOptions)
).then(done.fail, () => done());
});
it('can get security advice', async done => {
await reconfigureServer({
securityChecks: {
enableSecurityChecks: true,
enableLogOutput: true,
},
});
const options = Object.assign({}, masterKeyOptions, {
method: 'GET',
url: Parse.serverURL + '/securityChecks',
});
request(options).then(res => {
expect(res.data.CLP).not.toBeUndefined();
expect(res.data.ServerConfig).not.toBeUndefined();
expect(res.data.Files).not.toBeUndefined();
expect(res.data.Total).not.toBeUndefined();
done();
});
});

it('can get security on start', async done => {
await reconfigureServer({
securityChecks: {
enableSecurityChecks: true,
enableLogOutput: true,
},
});
const logger = require('../lib/logger').logger;
spyOn(logger, 'warn').and.callFake(() => {});
await new Promise(resolve => {
setTimeout(() => {
resolve();
}, 2000);
});
expect(logger.warn.calls.mostRecent().args[0]).toContain(
'Clients are currently allowed to create new classes.'
);
done();
});
});
47 changes: 47 additions & 0 deletions src/Adapters/Storage/Mongo/MongoStorageAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Parse from 'parse/node';
import _ from 'lodash';
import defaults from '../../../defaults';
import logger from '../../../logger';
import url from 'url';

// @flow-disable-next
const mongodb = require('mongodb');
Expand Down Expand Up @@ -187,6 +188,52 @@ export class MongoStorageAdapter implements StorageAdapter {
}
return this.client.close(false);
}
async getSecurityLogs(req: any) {
const options = req.config || req;
let databaseURI = options.databaseURI;
const warnings = [];
if (databaseURI.includes('@')) {
databaseURI = `mongodb://${databaseURI.split('@')[1]}`;
const pwd = options.databaseURI.split('//')[1].split('@')[0].split(':')[1] || '';
if (!pwd.match('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{14,})')) {
warnings.push({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here the weakness password detection is not appropriate since developers have their own secret policies (like no special char to avoid URL issues) depending of the company. So here may be just spawn a warning if process.env.NODE_ENV === production and password not detected into the mongo uri

Copy link
Member

@mtrezza mtrezza Dec 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In principle I agree, but Parse Server is also used by hobby developers for small projects. I think it's not a rare case that someone runs Parse Server on Heroku and connects to MongoDB Atlas, without having them isolated in a private cloud. A company with the skills to set up a private cloud is already more security aware than a hobby developer, who is more dependent on external security guidance that his feature provides.

Maybe keep this security check and introduce an option in the future to turn off certain checks that are not required?

title: `Weak Database Password`,
message:
'The database password set lacks complexity and length. This could potentially allow an attacker to brute force their way into the database, exposing the database.',
});
}
}
let databaseAdmin = '' + databaseURI;
try {
const parsedURI = url.parse(databaseAdmin);
parsedURI.port = '27017';
databaseAdmin = parsedURI.toString();
} catch (e) {
/* */
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only raise this db authentication warning in NODE_ENV production

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any harm logging this in development? Enabling checks depending on the environment is something I am critical of. We deny the developer to test out security checks in a safe playground. This can create a whole lot of confusion and false sense of security, especially since the PR currently does not log which checks are actually conducted.

try {
await MongoClient.connect(databaseAdmin, { useNewUrlParser: true });
warnings.push({
title: `Unrestricted access to port 27017`,
message:
'The database requires no authentication to the admin port. This could potentially allow an attacker to easily access the database, exposing all of the database.',
});
} catch (e) {
/* */
}
try {
await MongoClient.connect(databaseURI, { useNewUrlParser: true });
warnings.push({
title: `Unrestricted access to the database`,
message:
'The database requires no authentication to connect. This could potentially allow an attacker to easily access the database, exposing all of the database.',
});
} catch (e) {
/* */
}

return warnings;
}

_adaptiveCollection(name: string) {
return this.connect()
Expand Down
20 changes: 20 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,11 @@ module.exports.ParseServerOptions = {
action: parsers.numberParser('schemaCacheTTL'),
default: 5000,
},
securityChecks: {
env: 'PARSE_SERVER_SECURITY_CHECKS_OPTIONS',
help: 'View recommendations for server improvements',
action: parsers.objectParser,
},
serverCloseComplete: {
env: 'PARSE_SERVER_SERVER_CLOSE_COMPLETE',
help: 'Callback when server has closed',
Expand Down Expand Up @@ -552,6 +557,21 @@ module.exports.IdempotencyOptions = {
default: 300,
},
};
module.exports.SecurityChecksOptions = {
enableLogOutput: {
env: 'PARSE_SERVER_SECURITY_CHECKS_ENABLE_LOG_OUTPUT',
help:
'Is true if security warnings should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs.',
action: parsers.booleanParser,
default: false,
},
enableSecurityChecks: {
env: 'PARSE_SERVER_SECURITY_CHECKS_ENABLE_SECURITY_CHECKS',
help: 'If true if Parse Server should self-check the security of its current configuration.',
action: parsers.booleanParser,
default: false,
},
};
module.exports.AccountLockoutOptions = {
duration: {
env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION',
Expand Down
7 changes: 7 additions & 0 deletions src/Options/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
* @property {Boolean} revokeSessionOnPasswordReset When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.
* @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false.
* @property {Number} schemaCacheTTL The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.
* @property {SecurityChecksOptions} securityChecks View recommendations for server improvements
* @property {Function} serverCloseComplete Callback when server has closed
* @property {Function} serverStartComplete Callback when server has started
* @property {String} serverURL URL to your parse server with http:// or https://.
Expand Down Expand Up @@ -121,6 +122,12 @@
* @property {Number} ttl The duration in seconds after which a request record is discarded from the database, defaults to 300s.
*/

/**
* @interface SecurityChecksOptions
* @property {Boolean} enableLogOutput Is true if security warnings should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs.
* @property {Boolean} enableSecurityChecks If true if Parse Server should self-check the security of its current configuration.
*/

/**
* @interface AccountLockoutOptions
* @property {Number} duration number of minutes that a locked-out account remains locked out before automatically becoming unlocked.
Expand Down
12 changes: 12 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ export interface ParseServerOptions {
:ENV: PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS
:DEFAULT: false */
idempotencyOptions: ?IdempotencyOptions;
/* View recommendations for server improvements
:ENV: PARSE_SERVER_SECURITY_CHECKS_OPTIONS */
securityChecks: ?SecurityChecksOptions;
/* Full path to your GraphQL custom schema.graphql file */
graphQLSchema: ?string;
/* Mounts the GraphQL endpoint
Expand Down Expand Up @@ -292,6 +295,15 @@ export interface IdempotencyOptions {
ttl: ?number;
}

export interface SecurityChecksOptions {
/* If true if Parse Server should self-check the security of its current configuration.
:DEFAULT: false */
enableSecurityChecks: ?boolean;
/* Is true if security warnings should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs.
:DEFAULT: false */
enableLogOutput: ?boolean;
}

export interface AccountLockoutOptions {
/* number of minutes that a locked-out account remains locked out before automatically becoming unlocked. */
duration: ?number;
Expand Down
33 changes: 33 additions & 0 deletions src/ParseServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { AggregateRouter } from './Routers/AggregateRouter';
import { ParseServerRESTController } from './ParseServerRESTController';
import * as controllers from './Controllers';
import { ParseGraphQLServer } from './GraphQL/ParseGraphQLServer';
import { securityChecks } from './SecurityChecks.js';

// Mutate the Parse object to add the Cloud Code handlers
addParseCloud();
Expand Down Expand Up @@ -80,6 +81,9 @@ class ParseServer {
if (serverStartComplete) {
serverStartComplete();
}
if ((options.securityChecks || {}).enableLogOutput) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

this.getSecurityChecks();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run security check after serverStartComplete promise resolved (if serverStartComplete return a promise) to avoid false/positive, since developers can inject some initialization script into serverStartComplete 😉

})
.catch(error => {
if (serverStartComplete) {
Expand Down Expand Up @@ -109,6 +113,35 @@ class ParseServer {
return this._app;
}

async getSecurityChecks() {
const config = Config.get(this.config.appId);
const response = await securityChecks(config);
const logger = logging.getLogger();
const warnings = response.response;
const clp = warnings.CLP;
const total = warnings.Total;
if (total == 0) {
return;
}
let errorString = `${total} security warning(s) for Parse Server:\n\n`;
delete warnings.Total;
delete warnings.CLP;
for (const security in warnings) {
for (const issue of warnings[security]) {
errorString += ` -${issue.title}\n`;
errorString += ` ${issue.message}\n\n`;
}
}
for (const issue in clp) {
errorString += `\n Add CLP for Class: ${issue}\n`;
const classData = clp[issue];
for (const clpIssue of classData) {
errorString += ` ${clpIssue.title}\n`;
}
}
logger.warn(errorString);
}

handleShutdown() {
const promises = [];
const { adapter: databaseAdapter } = this.config.databaseController;
Expand Down
2 changes: 2 additions & 0 deletions src/Routers/CloudCodeRouter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import PromiseRouter from '../PromiseRouter';
import Parse from 'parse/node';
import rest from '../rest';
import { securityChecks } from '../SecurityChecks.js';
const triggers = require('../triggers');
const middleware = require('../middlewares');

Expand Down Expand Up @@ -53,6 +54,7 @@ export class CloudCodeRouter extends PromiseRouter {
middleware.promiseEnforceMasterKeyAccess,
CloudCodeRouter.deleteJob
);
this.route('GET', '/securityChecks', middleware.promiseEnforceMasterKeyAccess, securityChecks);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about removing the endpoint also if the options is not enabled ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the argument for removing the endpoint?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we should avoid exposing endpoints (even if it's protected by master key), if the feature is disabled ! Here, it is even more important because it provides advanced security feedback from the server.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related but i don't know if notifications works correctly: Also @mtrezza could you check your parse community inbox ? I would like to open a security advisory. But it seems that i do not have rights on the repo explain the issue.

Copy link
Member

@mtrezza mtrezza Dec 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we should avoid exposing endpoints (even if it's protected by master key), if the feature is disabled ! Here, it is even more important because it provides advanced security feedback from the server.

I expect that we will enable this feature by default in the future (as discussed previously in the related issue), and that most deployments will have this enabled. A production system should really prevent unauthorized access using firewall rules, we can add that to the docs as recommendation. The downside I see is that removing an endpoint means returning 404 but the correct response is really 403, either by Parse Server, or by a firewall. If someone decides that they want to obfuscate the 403 to a 404 based on their security strategy, that can still be done by the developer using a firewall, but I think that strategy should not be mandated by Parse Server.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right the endpoint should throw 403 if option not enabled !


static getJobs(req) {
Expand Down
Loading