-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Add Security Checks Log #6973
Changes from 15 commits
b7257cb
83aaf62
f70e002
060b922
d972e8c
61412fb
152a02b
47650b4
3a002fb
d332f05
c68ac63
52b3b76
b06fe25
1ae8fbc
77364f4
60dc661
aa935b4
373fcdf
379e84a
2188f70
fff6b7e
b4faede
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'); | ||
|
@@ -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({ | ||
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) { | ||
/* */ | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only raise this db authentication warning in NODE_ENV production There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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(); | ||
|
@@ -80,6 +81,9 @@ class ParseServer { | |
if (serverStartComplete) { | ||
serverStartComplete(); | ||
} | ||
if ((options.securityChecks || {}).enableLogOutput) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here |
||
this.getSecurityChecks(); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
@@ -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; | ||
|
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'); | ||
|
||
|
@@ -53,6 +54,7 @@ export class CloudCodeRouter extends PromiseRouter { | |
middleware.promiseEnforceMasterKeyAccess, | ||
CloudCodeRouter.deleteJob | ||
); | ||
this.route('GET', '/securityChecks', middleware.promiseEnforceMasterKeyAccess, securityChecks); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What would be the argument for removing the endpoint? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
There was a problem hiding this comment.
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
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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?