spartan-shield
Version:
nodejs project to package and configure common security middleware.
244 lines (238 loc) • 10.5 kB
JavaScript
var secJson = require('../security.json')
let validate = require('validate.js')
let sanitizeHtml = require('sanitize-html')
var whitelists = require('./.whitelists.json')
const tls = require('tls')
var validationMessages = {}
function passwordPolicy () {
return {
presence: true,
length: {
minimum: secJson.accessControlsPolicy.authenticationPolicy.passwords.minLen,
maximum: secJson.accessControlsPolicy.authenticationPolicy.passwords.maxLen
},
format: secJson.accessControlsPolicy.authenticationPolicy.passwords.regex
}
}
function policyCheck () {
if (secJson.contentValidationPolicy.enabled === false) {
let error = new Error('validation/disabled-by-policy')
error.message = 'Validation is disabled by policy'
return error
}
}
function typeCheck (obj, rules) {
if (!validate.isObj(obj) || !validate.isObj(rules)) {
let err = new TypeError('validation/invalid-type')
err.message = `Invalid Type Found. Cannot validate ${obj} against ${rules}`
return err
}
for (let rule in rules) {
for (let object in obj) {
if (rules[rule].type === 'string') {
validate.isString(obj[object])
} else if (rules[rule].type === 'date' || rules[rule].type === 'Date') {
validate.isDate(obj[object])
} else if (rules[rule].type === 'array' || rules[rule].type === 'Array') {
validate.isArray(obj[object])
} else if (rules[rule].type === 'object' || rules[rule].type === 'Object') {
validate.isObject(obj[object])
} else if (rule[rule].type === 'boolean' || rules[rule].type === 'Boolean') {
validate.isBoolean(obj[object])
} else if (rules[rule].type === 'number' || rules[rule].type === 'Number') {
validate.isNumber(obj[object])
} else if (rules[rule].type === 'integer' || rules[rule].type === 'Integer') {
validate.isInteger(obj[object])
} else if (rules[rule].type === 'function' || rules[rule].type === 'Function') {
// Not recommended to allow arbitrary functions to be passed as arguments
validate.isFunction(obj[object])
} else if (typeof obj[object] !== (rules[rule].type).toString()) {
let err = new TypeError('validation/input-type-error')
err.message = `Invalid input type. Expected ${rules[rule].type}. Instead found ${(typeof obj[object])}`
return err
} else {
let error = new TypeError('validation/input-type-error')
error.message = `Invalid input type. Expected ${rules[rule].type}. Instead found ${(typeof obj[object])}`
return error
}
}
}
}
function constraintBuilder (rules) {
let constraints = {}
function authSchema () {
for (let attrs in rules) {
if (rules[attrs] === 'password' && secJson.accessControlsPolicy.enabled && secJson.accessControlsPolicy.authenticationRequired) {
constraints.password.presence = true
constraints.password.format = {
pattern: secJson.accessControlsPolicy.authenticationPolicy.password.matches,
flags: 'i',
message: 'That is not a valid password'
}
constraints.password.length = {
is: secJson.accessControlsPolicy.authenticationPolicy.password.length,
wrongLength: 'Needs to be %{count} characters'
}
}
if (rules[attrs] === 'email' && secJson.accessControlsPolicy.enabled && secJson.accessControlsPolicy.authenticationRequired) {
constraints.email.presence = true
constraints.email.email = true
}
}
return constraints
}
// let's build the ruleset
for (let r in rules) {
// check to see if the attribute is required
if (rules[r] === 'required') {
constraints[rules[r]].presence = true
}
// now let's do local auth
for (let p = 0; p < secJson.accessControlsPolicy.authenticationPolicy.supportedMethods.length; p++) {
if (secJson.accessControlsPolicy.authenticationPolicy.supportedMethods[p] === 'uname/passwd' || secJson.accessControlsPolicy.authenticationPolicy.supportedMethods === 'local') {
constraints.password = authSchema().password
constraints.email = authSchema().email
}
}
// now more generic constraints
if (rules[r] === 'unique') {
constraints[rules[r]].unique = true
}
if (rules[r] === 'format') {
constraints[rules[r]].format = rules[r].format
}
if (rules[r] === 'length') {
constraints[rules[r]]['length'].is = rules[r].length
}
if (rules[r] === 'email') {
constraints[rules[r]].email = true
}
if (rules[r] === 'date' || rules[r] === 'datetime') {
constraints[rules[r]].datetime = true
}
if (rules[r] === 'matches') {
constraints[rules[r]].equality = rules[r].matches
}
}
}
module.exports = {
validated: (whatToValidate) => {
if (policyCheck() instanceof Error) {
return policyCheck()
}
if (whatToValidate.includes('header')) {
return function headerCheck (requestHeaders, callback) {
let whitelistRequired = secJson.contentValidationPolicy.semanticValidation.whitelistRequired
// for each header in requestHeaders
for (let header in requestHeaders) {
// check to see if the header requires a whitelist
// then check to see if THAT header's whitelist actually exists
if (whitelistRequired.includes(header)) {
if (whitelists[header]) {
// check to see if the VALUE of THAT header is on the whitelist
if (whitelists[header].includes(requestHeaders[header])) {
// send 'valid' to the callback
callback(null, 'valid')
} else {
// if it isn't, send an error to the callback function
let error = `${requestHeaders[header]} is invalid in header ${header}`
error.status = 401
callback(error)
}
} else {
// otherwise, report an error to the call back function that a whitelist for THAT header could not be found
let error = new Error('validate/whitelist-missing')
error.message = `Unable to check ${header}'s whitelist to validate ${requestHeaders[header]}`
callback(error, requestHeaders[header])
}
} else {
// if the header does not require a whitelist, report a MESSAGE in the callback that the header does not require a whitelist and its value was not validated
let message = { [header]: `Header ${header} does not require validation` }
validationMessages.headers = message
callback(null, message)
}
}
}
} else if (whatToValidate.includes('form')) {
return function formValidator (form, rules, callback) {
// build the constraints object for each element in the rules
let constraints = {}
for (let element in rules.fields) {
let valid = Object.keys(rules.fields[element].validation)
let values = rules.fields[element].validation
if (rules.fields.password) {
constraints.password = passwordPolicy()
}
if (valid.includes('required')) {
constraints[element] = { presence: true }
}
if (valid.includes('length')) {
constraints[element] = { length: { is: values.length } }
}
if (rules.fields.includes('email') || valid.includes('email')) {
constraints[element] = { email: true }
}
if (valid.includes('matches')) {
constraints[element] = { equality: values.matches }
}
if (valid.includes('excludes')) {
constraints[element] = { exclusion: { within: values.excludes } } // either a list (array) or an object
}
if (valid.includes('format')) {
constraints[element] = { format: values.format } // matches regular expression pattern
}
if (valid.includes('includes')) {
constraints[element] = { inclusion: { within: values.includes } } // either a list (array) or object
}
if (valid.includes('url')) {
constraints[element] = { url: true }
}
let check = validate({ element: form.body[element] }, constraints[element]) // validate that the value for that element matches the constraints defined in the rules
callback(null, check)
}
}
} else if (whatToValidate.includes('authentication')) {
return function authenticationCheck (user, callback) {
}
} else if (whatToValidate.includes('authorization')) {
return function authorizationCheck (role, callback) {
}
} else if (whatToValidate.includes('session')) {
return function sessionCheck (sessionId, callback) {
}
} else if (whatToValidate.includes('connection')) {
return function connectionCheck (requestOrigin, callback) {
// returns an error to the callback if the server is configured in a way that would contribute to a successful downgrade attack.
// Downgrade attacks happen like this:
// 1. Client uses weak or insecure SSL ciphers and the server DOESN'T reject it
let message = `These are the currently supported ciphers: ${tls.getCiphers()}`
validationMessages.ciphers = message
// 2. Sever offers both HTTP and HTTPS applications and DOES NOT redirect from HTTP
// 3. Server DOES not have HSTS enabled (thus not forcing a redirect from HTTP)
// HOWEVER if the application provides an HTTPS service AND has the HSTS header enabled, this check should pass
}
} else if (whatToValidate.includes('view')) {
return function sanitizeView (html, callback) {
let clean = sanitizeHtml(html)
return clean
}
} else {
let message = 'Validation failed'
return message
}
},
contentValidation: (obj, rules, callback) => {
if (policyCheck() instanceof Error) {
return policyCheck()
}
if (typeCheck(obj, rules) instanceof Error) {
return typeCheck(obj, rules)
}
let constraints = constraintBuilder(rules)
callback(null, validate(obj, constraints))
},
uploadCheck: (file, callback) => {
// check file uploads (check file type, filename, file size, rename file before storage (including extension), set content type)
}
}