-
-
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 all 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,76 @@ | ||
'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.ServerConfiguration).not.toBeUndefined(); | ||
expect(res.data.Database).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); | ||
}); | ||
let messagesCalled = ''; | ||
for (const item in logger.warn.calls.all()) { | ||
const call = logger.warn.calls.all()[item]; | ||
messagesCalled = messagesCalled + ' ' + (call.args || []).join(' '); | ||
} | ||
expect(messagesCalled).toContain('Clients are currently allowed to create new classes.'); | ||
done(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1057,5 +1057,4 @@ export class MongoStorageAdapter implements StorageAdapter { | |
}); | ||
} | ||
} | ||
|
||
export default MongoStorageAdapter; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,6 +41,8 @@ import { AggregateRouter } from './Routers/AggregateRouter'; | |
import { ParseServerRESTController } from './ParseServerRESTController'; | ||
import * as controllers from './Controllers'; | ||
import { ParseGraphQLServer } from './GraphQL/ParseGraphQLServer'; | ||
import SecurityCheck from './SecurityCheck'; | ||
import { registerServerSecurityChecks } from './SecurityChecks'; | ||
|
||
// Mutate the Parse object to add the Cloud Code handlers | ||
addParseCloud(); | ||
|
@@ -79,7 +81,19 @@ class ParseServer { | |
Promise.all([dbInitPromise, hooksLoadPromise]) | ||
.then(() => { | ||
if (serverStartComplete) { | ||
serverStartComplete(); | ||
return serverStartComplete(); | ||
} | ||
return Promise.resolve(); | ||
}) | ||
.then(() => { | ||
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. This should never be undefined, instead set a default value in option definitions. |
||
return registerServerSecurityChecks(this.config); | ||
} | ||
return Promise.resolve(); | ||
}) | ||
.then(() => { | ||
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(); | ||
} | ||
}) | ||
.catch(error => { | ||
|
@@ -110,6 +124,28 @@ class ParseServer { | |
return this._app; | ||
} | ||
|
||
async getSecurityChecks() { | ||
const checks = await SecurityCheck.getChecks(); | ||
const logger = logging.getLogger(); | ||
const { Total } = checks; | ||
delete checks.Total; | ||
for (const category in checks) { | ||
const data = checks[category]; | ||
for (const check of data) { | ||
const { title, warning, error, result, success } = check; | ||
if (result === 'success') { | ||
logger.warn(`✅ ${success}\n`); | ||
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. All this logic should go into the security checks files, not into the server file. |
||
} else { | ||
const appendString = error && error !== 'Check failed.' ? ` with error: ${error}` : ''; | ||
logger.warn(`❌ ${warning}\n❗ Check "${title}" failed${appendString}\n`); | ||
} | ||
} | ||
} | ||
if (Total !== 0) { | ||
logger.warn(`❗ ${Total} security warning(s) for Parse Server`); | ||
} | ||
} | ||
|
||
handleShutdown() { | ||
const promises = []; | ||
const { adapter: databaseAdapter } = this.config.databaseController; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import logger from './logger'; | ||
class SecurityCheck { | ||
constructor(data) { | ||
const { group, title, warning, check, failed, success } = data; | ||
try { | ||
if (!group || !title || !warning) { | ||
throw 'Security checks must have a group, title, and a warning.'; | ||
} | ||
if (typeof group !== 'string') { | ||
throw '"group" of the security check must be a string, e.g SecurityCheck.Category.Database'; | ||
} | ||
if (typeof success !== 'string') { | ||
throw '"success" message of the security check must be a string.'; | ||
} | ||
if (typeof title !== 'string') { | ||
throw '"title" of the security check must be a string.'; | ||
} | ||
if (typeof warning !== 'string') { | ||
throw '"warning" message of the security check must be a string.'; | ||
} | ||
if (check && typeof check !== 'function') { | ||
throw '"check" of the security check must be a function.'; | ||
} | ||
this.group = group; | ||
this.title = title; | ||
this.warning = warning; | ||
this.check = check; | ||
this.failed = failed; | ||
this.success = success; | ||
} catch (e) { | ||
logger.error(e); | ||
return; | ||
} | ||
_registerCheck(this); | ||
} | ||
async run() { | ||
try { | ||
if (this.failed) { | ||
throw 'Check failed.'; | ||
} | ||
if (!this.check) { | ||
return { | ||
result: 'success', | ||
}; | ||
} | ||
const result = await this.check(); | ||
if (result != null && result === false) { | ||
throw 'Check failed.'; | ||
} | ||
return { | ||
result: 'success', | ||
}; | ||
} catch (error) { | ||
return { | ||
result: 'fail', | ||
error, | ||
}; | ||
} | ||
} | ||
setFailed() { | ||
this.failed = true; | ||
} | ||
} | ||
SecurityCheck.Category = { | ||
Database: 'Database', | ||
CLP: 'CLP', | ||
ServerConfiguration: 'ServerConfiguration', | ||
}; | ||
SecurityCheck.getChecks = async () => { | ||
const resultsByGroup = {}; | ||
let total = 0; | ||
const resolveSecurityCheck = async check => { | ||
const { group, title, warning, success } = check; | ||
const { result, error } = await check.run(); | ||
const category = resultsByGroup[group] || []; | ||
category.push({ | ||
title, | ||
warning, | ||
error, | ||
result, | ||
success, | ||
}); | ||
resultsByGroup[group] = category; | ||
if (result !== 'success') { | ||
total++; | ||
} | ||
}; | ||
await Promise.all(securityCheckStore.map(check => resolveSecurityCheck(check))); | ||
resultsByGroup.Total = total; | ||
return resultsByGroup; | ||
}; | ||
const securityCheckStore = []; | ||
function _registerCheck(securityCheck) { | ||
for (const [i, check] of securityCheckStore.entries()) { | ||
if (check.title == securityCheck.title && check.warning == securityCheck.warning) { | ||
securityCheckStore[i] = securityCheck; | ||
return; | ||
} | ||
} | ||
securityCheckStore.push(securityCheck); | ||
} | ||
module.exports = SecurityCheck; |
Uh oh!
There was an error while loading. Please reload this page.