Comparing version 1.1.0 to 2.0.0
@@ -0,1 +1,8 @@ | ||
#### v1.1.0 | ||
* Added CSRF `safeVerbs` option. | ||
* Multiple code improvements. | ||
* Improved code docs. | ||
#### v1.0.2 | ||
@@ -2,0 +9,0 @@ |
74
index.js
@@ -1,73 +0,1 @@ | ||
'use strict'; | ||
/** | ||
* Fi Aegis. | ||
* | ||
* @module fi-aegis | ||
* | ||
* @see module:fi-aegis | ||
*/ | ||
/** | ||
* Configures the module. | ||
* | ||
* @param {Object} options The options object. | ||
* @param {Object} options.csp The options for the `csp` module. | ||
* @param {Object} options.csrf The options for the `csrf` module. | ||
* @param {Object} options.hsts The options for the `hsts` module. | ||
* @param {Boolean} options.nosniff Whether to activate the `nosniff` module. | ||
* @param {String} options.p3p The header value for the `p3p` module. | ||
* @param {String} options.xframes The header value for the `xframe` module. | ||
* @param {Object} options.xssprotection The options for the `xssprotection` | ||
* module. | ||
* | ||
* @returns {Function} The Express middleware. | ||
*/ | ||
var aegis = module.exports = options => { | ||
const components = []; | ||
if (options) { | ||
Object.keys(aegis).forEach(key => { | ||
let config = options[key]; | ||
if (config) { | ||
components.push(aegis[key](config)); | ||
} | ||
}); | ||
} | ||
/** | ||
* Fi Aegis middleware. | ||
* | ||
* @param {Object} req Express request object. | ||
* @param {Object} res Express response object. | ||
* @param {Function} next Express next middleware callback. | ||
*/ | ||
function middleware(req, res, next) { | ||
var chain = next; | ||
components.forEach(component => { | ||
chain = (next => err => { | ||
if (err) { | ||
return next(err); | ||
} | ||
component(req, res, next); | ||
})(chain); | ||
}); | ||
chain(); | ||
} | ||
return middleware; | ||
}; | ||
aegis.csrf = require('./lib/csrf'); | ||
aegis.csp = require('./lib/csp'); | ||
aegis.hsts = require('./lib/hsts'); | ||
aegis.p3p = require('./lib/p3p'); | ||
aegis.xframe = require('./lib/xframes'); | ||
aegis.xssProtection = require('./lib/xssprotection'); | ||
aegis.nosniff = require('./lib/nosniff'); | ||
module.exports = require('./lib'); |
@@ -10,8 +10,6 @@ /** | ||
'use strict'; | ||
const { CSP_INVALID_POLICY } = require('./errors'); | ||
const ERR = require('./errors'); | ||
let header, value; | ||
var value, name; | ||
/** | ||
@@ -24,5 +22,4 @@ * CSP middleware. | ||
*/ | ||
function middleware(req, res, next) { | ||
res.header(name, value); | ||
function middleware (req, res, next) { | ||
res.header(header, value); | ||
next(); | ||
@@ -32,2 +29,33 @@ } | ||
/** | ||
* Creates a CSP policy string. | ||
* | ||
* @param {Array|Object|String} policy The policy object to parse. | ||
* | ||
* @returns {String} The CSP policy string. | ||
*/ | ||
function createPolicyString (policy) { | ||
if (typeof policy === 'string') { | ||
return policy; | ||
} | ||
if (Array.isArray(policy)) { | ||
return policy.map(csp.createPolicyString).join('; '); | ||
} | ||
if (typeof policy === 'object' && policy !== null) { | ||
let entries = Object.keys(policy).map(directive => { | ||
if (policy[String(directive)] === 0 || policy[String(directive)]) { | ||
directive += ` ${policy[String(directive)]}`; | ||
} | ||
return directive; | ||
}); | ||
return csp.createPolicyString(entries); | ||
} | ||
throw new Error(CSP_INVALID_POLICY); | ||
} | ||
/** | ||
* Configures the CSP module. | ||
@@ -42,7 +70,6 @@ * | ||
*/ | ||
function csp(options) { | ||
var reportUri, policyRules; | ||
function csp (options) { | ||
let reportUri, policyRules; | ||
let isReportOnly = false; | ||
var isReportOnly = false; | ||
if (options) { | ||
@@ -54,6 +81,6 @@ isReportOnly = options.reportOnly; | ||
name = 'content-security-policy'; | ||
header = 'Content-Security-Policy'; | ||
if (isReportOnly) { | ||
name += '-report-only'; | ||
header += '-Report-Only'; | ||
} | ||
@@ -70,3 +97,3 @@ | ||
value += 'report-uri ' + reportUri; | ||
value += `report-uri ${reportUri}`; | ||
} | ||
@@ -77,37 +104,4 @@ | ||
/** | ||
* Creates a CSP policy string. | ||
* | ||
* @param {Array|Object|String} policy The policy object to parse. | ||
* | ||
* @returns {String} The CSP policy string. | ||
*/ | ||
function createPolicyString(policy) { | ||
var entries; | ||
if (typeof policy === 'string') { | ||
return policy; | ||
} | ||
if (Array.isArray(policy)) { | ||
return policy.map(csp.createPolicyString).join('; '); | ||
} | ||
if (typeof policy === 'object' && policy !== null) { | ||
entries = Object.keys(policy).map(directive => { | ||
if (policy[directive] === 0 || policy[directive]) { | ||
directive += ' ' + policy[directive]; | ||
} | ||
return directive; | ||
}); | ||
return csp.createPolicyString(entries); | ||
} | ||
throw new Error(ERR.CSP_INVALID_POLICY); | ||
} | ||
csp.createPolicyString = createPolicyString; | ||
module.exports = csp; | ||
module.exports = csp; |
@@ -10,9 +10,6 @@ /** | ||
'use strict'; | ||
const { CSRF_TOKEN_MISSING, CSRF_TOKEN_MISMATCH } = require('./errors'); | ||
const token = require('./token'); | ||
const ERR = require('./errors'); | ||
const CONFIG = { | ||
const config = { | ||
safeVerbs: null, | ||
@@ -34,6 +31,6 @@ cookie: null, | ||
*/ | ||
function getCsrf(req, secret) { | ||
var csrf = CONFIG.impl.create(req, secret); | ||
var validate = CONFIG.impl.validate || csrf.validate; | ||
var token = csrf.token || csrf; | ||
function getCsrf (req, secret) { | ||
const csrf = config.impl.create(req, secret); | ||
const validate = config.impl.validate || csrf.validate; | ||
const token = csrf.token || csrf; | ||
@@ -55,7 +52,7 @@ secret = csrf.secret; | ||
*/ | ||
function setToken(res, token) { | ||
res.locals[CONFIG.key] = token; | ||
function setToken (res, token) { | ||
res.locals[config.key] = token; | ||
if (CONFIG.cookie && CONFIG.cookie.name) { | ||
res.cookie(CONFIG.cookie.name, token, CONFIG.cookie.options); | ||
if (config.cookie && config.cookie.name) { | ||
res.cookie(config.cookie.name, token, config.cookie.options); | ||
} | ||
@@ -73,7 +70,6 @@ } | ||
*/ | ||
function middleware(req, res, next) { | ||
var csrf = getCsrf(req, CONFIG.secret); | ||
function middleware (req, res, next) { | ||
let csrf = getCsrf(req, config.secret); | ||
let token; | ||
var token; | ||
setToken(res, csrf.token); | ||
@@ -87,3 +83,3 @@ | ||
req.csrfToken = () => { | ||
var newCsrf = getCsrf(req, CONFIG.secret); | ||
let newCsrf = getCsrf(req, config.secret); | ||
@@ -102,10 +98,15 @@ if (csrf.secret && newCsrf.secret && csrf.secret === newCsrf.secret) { | ||
/* Move along for safe verbs */ | ||
if (CONFIG.safeVerbs.indexOf(req.method) >= 0) { | ||
if (config.safeVerbs.indexOf(req.method) >= 0) { | ||
return next(); | ||
} | ||
/* Validate token */ | ||
token = (req.body && req.body[CONFIG.key]) || | ||
req.headers[CONFIG.header]; | ||
token = (req.body && req.body[String(config.key)]) || | ||
req.headers[String(config.header)]; | ||
if (!token) { | ||
console.info(req.headers); | ||
} | ||
if (csrf.validate(req, token)) { | ||
@@ -115,11 +116,9 @@ return next(); | ||
console.log(req.body, req.headers[CONFIG.header]); | ||
res.status(403); | ||
next(new Error(!token ? ERR.CSRF_TOKEN_MISSING : ERR.CSRF_TOKEN_MISMATCH)); | ||
next(new Error(!token ? CSRF_TOKEN_MISSING : CSRF_TOKEN_MISMATCH)); | ||
} | ||
/** | ||
* Confures the CSRF module. | ||
* Configures the CSRF module. | ||
* | ||
@@ -141,13 +140,10 @@ * @param {Object} options The options object. | ||
*/ | ||
module.exports = options => { | ||
options = options || {}; | ||
module.exports = (options = {}) => { | ||
/* Initialize defaults */ | ||
CONFIG.header = (options.header || 'csrf-token').toLowerCase(); // https://stackoverflow.com/a/5259004/1970170 | ||
CONFIG.safeVerbs = options.safeVerbs || ['OPTIONS', 'HEAD', 'GET']; | ||
CONFIG.secret = options.secret || '_csrfSecret'; | ||
CONFIG.impl = options.impl || token; | ||
CONFIG.key = options.key || '_csrf'; | ||
CONFIG.cookie = { | ||
config.header = (options.header || 'csrf-token').toLowerCase(); | ||
config.safeVerbs = options.safeVerbs || ['OPTIONS', 'HEAD', 'GET']; | ||
config.secret = options.secret || '_csrfSecret'; | ||
config.impl = options.impl || token; | ||
config.key = options.key || '_csrf'; | ||
config.cookie = { | ||
options: options.cookie && options.cookie.options, | ||
@@ -159,12 +155,11 @@ name: 'CSRF-TOKEN' | ||
if (options.angular) { | ||
CONFIG.cookie.name = 'XSRF-TOKEN'; | ||
CONFIG.header = 'x-xsrf-token'; | ||
config.cookie.name = 'XSRF-TOKEN'; | ||
config.header = 'x-xsrf-token'; | ||
} else if (typeof options.cookie === 'string') { | ||
CONFIG.cookie.name = options.cookie; | ||
config.cookie.name = options.cookie; | ||
} else if (options.cookie && typeof options.cookie.name === 'string') { | ||
CONFIG.cookie.name = options.cookie.name; | ||
config.cookie.name = options.cookie.name; | ||
} | ||
return middleware; | ||
}; | ||
}; |
@@ -1,3 +0,1 @@ | ||
'use strict'; | ||
/** | ||
@@ -10,15 +8,11 @@ * Prefixes error string for easier debugging. | ||
*/ | ||
function msg(message) { | ||
return `Fi Aegis: ${ message }`; | ||
function msg (message) { | ||
return `Fi Aegis: ${message}`; | ||
} | ||
module.exports = { | ||
SESSION_INVALID: msg('A valid session must be available in order to maintain state!'), | ||
CSP_INVALID_POLICY: msg('Invalid CSP policy! Must be Array, String, or Object.'), | ||
CSRF_TOKEN_MISMATCH: msg('CSRF token mismatch!'), | ||
CSRF_TOKEN_MISSING: msg('CSRF token missing!') | ||
}; | ||
}; |
@@ -10,8 +10,4 @@ /** | ||
'use strict'; | ||
let value; | ||
const HEADER = 'strict-transport-security'; | ||
var value; | ||
/** | ||
@@ -24,5 +20,5 @@ * HSTS Middleware. | ||
*/ | ||
function middleware(req, res, next) { | ||
function middleware (req, res, next) { | ||
if (value) { | ||
res.header(HEADER, value); | ||
res.header('Strict-Transport-Security', value); | ||
} | ||
@@ -44,6 +40,3 @@ | ||
*/ | ||
module.exports = options => { | ||
options = options || {}; | ||
module.exports = (options = {}) => { | ||
if (typeof options.maxAge !== undefined) { | ||
@@ -54,3 +47,3 @@ options.maxAge = Math.max(0, parseInt(options.maxAge)); | ||
if (options.maxAge > -1) { | ||
value = 'max-age=' + options.maxAge; | ||
value = `max-age=${options.maxAge}`; | ||
@@ -67,3 +60,2 @@ if (options.includeSubDomains) { | ||
return middleware; | ||
}; | ||
}; |
@@ -10,7 +10,2 @@ /** | ||
'use strict'; | ||
const HEADER = 'x-content-type-options'; | ||
const VALUE = 'nosniff'; | ||
/** | ||
@@ -23,5 +18,4 @@ * Nosniff Middleware. | ||
*/ | ||
function middleware(req, res, next) { | ||
res.header(HEADER, VALUE); | ||
function middleware (req, res, next) { | ||
res.header('X-Content-Type-Options', 'nosniff'); | ||
next(); | ||
@@ -31,6 +25,6 @@ } | ||
/** | ||
* Configures the Nosniff module. | ||
* Configures the NoSniff module. | ||
* | ||
* @returns {Function} The Express middleware. | ||
*/ | ||
module.exports = () => middleware; | ||
module.exports = () => middleware; |
@@ -1,6 +0,4 @@ | ||
'use strict'; | ||
const crypto = require('crypto'); | ||
const ERR = require('./errors'); | ||
const { SESSION_INVALID } = require('./errors'); | ||
@@ -17,4 +15,8 @@ const LENGTH = 10; | ||
*/ | ||
function tokenize(salt, secret) { | ||
return salt + crypto.createHash('sha1').update(salt + secret).digest('base64'); | ||
function tokenize (salt, secret) { | ||
const hash = crypto.createHash('sha1') | ||
.update(`${salt}${secret}`) | ||
.digest('base64'); | ||
return `${salt}${hash}`; | ||
} | ||
@@ -25,15 +27,19 @@ | ||
* | ||
* @param {Number} len The salt's length. | ||
* @param {Number} length The salt's length. | ||
* | ||
* @returns {String} The generated salt. | ||
*/ | ||
function salt(len) { | ||
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | ||
var str = ''; | ||
function salt (length) { | ||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | ||
const randomBytes = crypto.randomBytes(length); | ||
const result = new Array(length); | ||
for (var i = 0; i < len; i++) { | ||
str += chars[Math.floor(Math.random() * chars.length)]; | ||
let cursor = 0; | ||
for (let i = 0; i < length; i++) { | ||
cursor += randomBytes[parseInt(i)]; | ||
result[parseInt(i)] = chars[parseInt(cursor % chars.length)]; | ||
} | ||
return str; | ||
return result.join(''); | ||
} | ||
@@ -50,3 +56,3 @@ | ||
*/ | ||
function validate(secretKey, req, token) { | ||
function validate (secretKey, req, token) { | ||
if (typeof token !== 'string') { | ||
@@ -56,3 +62,6 @@ return false; | ||
return token === tokenize(token.slice(0, LENGTH), req.session[secretKey]); | ||
const key = req.session[String(secretKey)]; | ||
const slice = token.slice(0, LENGTH); | ||
return token === tokenize(slice, key); | ||
} | ||
@@ -68,15 +77,15 @@ | ||
*/ | ||
function create(req, secretKey) { | ||
var session = req.session; | ||
function create (req, secretKey) { | ||
const { session } = req; | ||
if (session === undefined) { | ||
throw new Error(ERR.SESSION_INVALID); | ||
if (!session) { | ||
throw new Error(SESSION_INVALID); | ||
} | ||
/* Save the secret for validation */ | ||
var secret = session[secretKey]; | ||
let secret = session[String(secretKey)]; | ||
if (!secret) { | ||
session[secretKey] = crypto.pseudoRandomBytes(LENGTH).toString('base64'); | ||
secret = session[secretKey]; | ||
session[String(secretKey)] = crypto.randomBytes(LENGTH).toString('base64'); | ||
secret = session[String(secretKey)]; | ||
} | ||
@@ -91,4 +100,2 @@ | ||
module.exports = { | ||
create: create | ||
}; | ||
module.exports = { create }; |
@@ -10,8 +10,4 @@ /** | ||
'use strict'; | ||
let value; | ||
const HEADER = 'x-frame-options'; | ||
var value; | ||
/** | ||
@@ -24,5 +20,5 @@ * X-Frame-Options middleware. | ||
*/ | ||
function middleware(req, res, next) { | ||
function middleware (req, res, next) { | ||
if (value) { | ||
res.header(HEADER, value); | ||
res.header('X-Frame-Options', value); | ||
} | ||
@@ -41,7 +37,5 @@ | ||
module.exports = val => { | ||
value = val; | ||
return middleware; | ||
}; | ||
}; |
@@ -10,8 +10,4 @@ /** | ||
'use strict'; | ||
let value; | ||
const HEADER = 'x-xss-protection'; | ||
var value; | ||
/** | ||
@@ -24,5 +20,5 @@ * X-Frame-Options middleware. | ||
*/ | ||
function middleware(req, res, next) { | ||
function middleware (req, res, next) { | ||
if (value) { | ||
res.header(HEADER, value); | ||
res.header('X-XSS-Protection', value); | ||
} | ||
@@ -44,6 +40,3 @@ | ||
*/ | ||
module.exports = options => { | ||
options = options || {}; | ||
module.exports = (options = {}) => { | ||
/* IMPORTANT: `enabled` should be either `1` or `0` */ | ||
@@ -60,3 +53,3 @@ if (typeof options === 'boolean') { | ||
var mode = 'block'; | ||
let mode = 'block'; | ||
@@ -67,6 +60,5 @@ if (options && options.mode && typeof options.mode === 'string') { | ||
value += '; mode=' + mode; | ||
value += `; mode=${mode}`; | ||
return middleware; | ||
}; | ||
}; |
{ | ||
"name": "fi-aegis", | ||
"version": "1.1.0", | ||
"version": "2.0.0", | ||
"description": "Web Application Security Middleware.", | ||
"author": "Jeff Harrell <jeharrell@paypal.com>", | ||
"homepage": "https://github.com/finaldevstudio/fi-aegis", | ||
"author": "Santiago G. Marín <santiago.marin@dotstudio.io>", | ||
"homepage": "https://github.com/dotstudio-io/fi-aegis", | ||
"main": "index", | ||
"license": "MIT", | ||
"scripts": { | ||
"test": "node_modules/.bin/mocha" | ||
"test": "nyc mocha" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/finaldevstudio/fi-aegis.git" | ||
"url": "git+https://github.com/dotstudio-io/fi-aegis.git" | ||
}, | ||
"contributors": [ | ||
"Santiago G. Marín <santiago@finaldevstudio.com>" | ||
"Santiago G. Marín <santiago.marin@dotstudio.io>" | ||
], | ||
"engines": { | ||
"node": ">=4.0.0", | ||
"npm": ">=3.0.0" | ||
"node": "12", | ||
"npm": ">=6" | ||
}, | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"body-parser": "^1.6.3", | ||
"chance": "^1.0.10", | ||
"cookie-parser": "^1.3.2", | ||
"cookie-session": "^1.0.2", | ||
"data-driven": "^1.0.0", | ||
"errorhandler": "^1.1.1", | ||
"express": "^4.3.8", | ||
"express-session": "^1.7.5", | ||
"mocha": "^3.4.2", | ||
"supertest": "^3.0.0" | ||
"body-parser": "^1.19.0", | ||
"chance": "^1.1.4", | ||
"cookie-parser": "^1.4.4", | ||
"cookie-session": "^1.3.3", | ||
"data-driven": "^1.4.0", | ||
"errorhandler": "^1.5.1", | ||
"eslint-plugin-mocha": "^6.2.2", | ||
"eslint-plugin-node": "^11.0.0", | ||
"eslint-plugin-security": "^1.4.0", | ||
"express": "^4.17.1", | ||
"express-session": "^1.17.0", | ||
"mocha": "^6.2.2", | ||
"nyc": "^15.0.0", | ||
"supertest": "^4.0.2" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/finaldevstudio/fi-aegis/issues", | ||
"email": "security@finaldevstudio.com" | ||
"url": "https://github.com/dotstudio-io/fi-aegis/issues", | ||
"email": "security@dotstudio.io" | ||
}, | ||
@@ -40,0 +44,0 @@ "directories": { |
@@ -44,15 +44,14 @@ # Fi Aegis | ||
app.use(aegis({ | ||
csrf: true, | ||
csp: { | ||
xframe: 'SAMEORIGIN', | ||
xssProtection: true, | ||
nosniff: true, | ||
csrf: { | ||
angular: true | ||
}, | ||
xframe: 'SAMEORIGIN', | ||
p3p: 'ABCDEF', /*[DEPRECATED]*/ | ||
csp: , | ||
hsts: { | ||
includeSubDomains: true, | ||
maxAge: 31536000, | ||
includeSubDomains: true, | ||
preload: true | ||
}, | ||
xssProtection: true, | ||
nosniff: true | ||
} | ||
})); | ||
@@ -105,5 +104,5 @@ ``` | ||
| `angular` | `Boolean` | No | `false` | Shorthand setting to set **Fi Aegis** up to use the default settings for CSRF validation according to the [AngularJS docs](https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection). | | ||
| `cookie` | `String` or `Object` | No | `Object` | If set, a cookie with the name you provide will be set with the CSRF token. | | ||
| `cookie` | `String` or `Object` | No | `Object` | A cookie with the name you provide will be set with the CSRF token. | | ||
| `cookie.name` | `String` | No | `CSRF-TOKEN` | The name you provide will be set as the cookie with the CSRF token. | | ||
| `cookie.options` | `Object` | No | `{}` | A valid Express cookie options object. See [Express' res.cookie](http://expressjs.com/en/4x/api.html#res.cookie) API docs for more information. | | ||
| `cookie.options` | `Object` | No | `{}` | A valid Express cookie options object. See [Express res.cookie](http://expressjs.com/en/4x/api.html#res.cookie) API docs for more information. | | ||
| `header` | `String` | No | `csrf-token` | If set, the header name you provide will be expected to have the CSRF token. | | ||
@@ -293,2 +292,2 @@ | `safeVerbs` | `[String]` | No | `['OPTIONS', 'HEAD', 'GET']` | A list of HTTP verbs cosidered `safe` that will skip CSRF token validation. | ||
|-------|------|----------|---------|-------------| | ||
| `value` | `String` | Yes | None | The compact privacy policy. | | ||
| `value` | `String` | Yes | None | The compact privacy policy. | |
# Security Policy | ||
Security is a very important part of our applications and therefore must be treated seriously and professionaly. | ||
Security is a very important part of our applications and therefore must be treated seriously and professionally. | ||
@@ -8,5 +8,5 @@ | ||
If you think you may have found a bug or flaw please [open an issue](https://github.com/FinalDevStudio/fi-aegis/issues/new) so everyone can help solve it as quickly as possible. | ||
If you think you may have found a bug or flaw please[open an issue](https://github.com/dotstudio-io/fi-aegis/issues/new) so everyone can help solve it as quickly as possible. | ||
If the issue is too risky to be put out in the open, please send us an email with the details to [security@finaldevstudio.com](mailto:security@finaldevstudio.com). | ||
If the issue is too risky to be put out in the open, please send us an email with the details to [santiago.marin@dotstudio.io](mailto:santiago.marin@dotstudio.io). | ||
@@ -18,7 +18,4 @@ | ||
If you feel we're not responding back in time, please send an email directly to [santiago@finaldevstudio.com](mailto:santiago@finaldevstudio.com) with a link to the issue or indicating that a previous message was sent. | ||
## History | ||
No reported issues. | ||
No reported issues. |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
237095
41
1032
14
1