Comparing version 3.1.0 to 3.2.0
202
lib/index.js
@@ -6,2 +6,4 @@ // Load modules | ||
var Cryptiles = require('cryptiles'); | ||
var Joi = require('joi'); | ||
var schema = require('./schema'); | ||
@@ -22,6 +24,8 @@ | ||
restful: false, // Set to true for X-CSRF-Token header crumb validation. Disables payload/query validation | ||
skip: false // Set to a function which returns true when to skip crumb generation and validation | ||
skip: false, // Set to a function which returns true when to skip crumb generation and validation | ||
allowOrigins: [] // A list of CORS origins to set crumb cookie on | ||
}; | ||
// Not used in restful mode | ||
internals.routeDefaults = { | ||
@@ -33,123 +37,163 @@ key: 'crumb', // query or payload key | ||
// Parses allowOrigin setting | ||
exports.register = function (plugin, options, next) { | ||
internals.originParser = function(origin, allowOrigins) { | ||
var settings = Hoek.applyToDefaults(internals.defaults, options); | ||
// copy the key and restful settings from internals.defaults to internals.routeDefaults for consistency | ||
internals.routeDefaults.key = settings.key; | ||
internals.routeDefaults.restful = settings.restful; | ||
this._origin = origin.split(':'); | ||
this._originPort = this._origin.length === 2 ? this._origin[1] : null; | ||
this._originParts = this._origin[0].split('.'); | ||
this._match = false; | ||
plugin.state(settings.key, settings.cookieOptions); | ||
for (var i = 0, allowOriginsLen = allowOrigins.length; i < allowOriginsLen; i++) { | ||
this._originAllow = allowOrigins[i].split(':'); | ||
this._originAllowPort = this._originAllow.length === 2 ? this._originAllow[1] : null; | ||
this._originAllowParts = this._originAllow[0].split('.'); | ||
plugin.ext('onPostAuth', function (request, reply) { | ||
// If skip function enabled. Call it and if returns true, do not attempt to do anything with crumb. | ||
if (settings.skip && typeof settings.skip === 'function' && settings.skip(request, reply)) { | ||
return reply(); | ||
if ((this._originPort && !this._originAllowPort) || (!this._originPort && this._originAllowPort) || (this._originAllowPort !== '*' && this._originPort !== this._originAllowPort)) { | ||
this._match = false; | ||
} | ||
// Validate incoming crumb | ||
if (typeof request.route.plugins._crumb === 'undefined') { | ||
if (request.route.plugins.crumb || | ||
!request.route.plugins.hasOwnProperty('crumb') && settings.autoGenerate) { | ||
request.route.plugins._crumb = Hoek.applyToDefaults(internals.routeDefaults, request.route.plugins.crumb || {}); | ||
else { | ||
for (var ii = 0, allowOriginPartsLen = this._originAllowParts.length; ii < allowOriginPartsLen; ii++) { | ||
this._match = this._originAllowParts[ii] === '*' || this._originAllowParts[ii] === this._originParts[ii]; | ||
if (!this._match) { | ||
break; | ||
} | ||
} | ||
else { | ||
request.route.plugins._crumb = false; | ||
if (this._match) { | ||
return this._match; | ||
} | ||
} | ||
} | ||
return this._match; | ||
} | ||
// Set crumb cookie and calculate crumb | ||
if ((settings.autoGenerate || | ||
request.route.plugins._crumb) && | ||
!request.server.settings.cors) { | ||
exports.register = function (plugin, options, next) { | ||
generate(request, reply); | ||
Joi.validate(options, schema, { convert: false }, function (err, value) { | ||
if (err) { | ||
//plugin.hapi.error.internal('Invalid plugin options for crumb', err); | ||
return next('Invalid plugin options for crumb: ' + JSON.stringify(err)); | ||
} | ||
// Validate crumb | ||
var settings = Hoek.applyToDefaults(internals.defaults, options); | ||
// copy the key and restful settings from internals.defaults to internals.routeDefaults for consistency | ||
internals.routeDefaults.key = settings.key; | ||
internals.routeDefaults.restful = settings.restful; | ||
if (settings.restful === false || | ||
(!request.route.plugins._crumb || request.route.plugins._crumb.restful === false)) { | ||
plugin.state(settings.key, settings.cookieOptions); | ||
if (request.method !== 'post' || | ||
!request.route.plugins._crumb) { | ||
plugin.ext('onPostAuth', function (request, reply) { | ||
// If skip function enabled. Call it and if returns true, do not attempt to do anything with crumb. | ||
if (settings.skip && typeof settings.skip === 'function' && settings.skip(request, reply)) { | ||
return reply(); | ||
} | ||
var content = request[request.route.plugins._crumb.source]; | ||
if (content instanceof Stream) { | ||
// Validate incoming crumb | ||
return reply(plugin.hapi.error.forbidden()); | ||
} | ||
if (typeof request.route.plugins._crumb === 'undefined') { | ||
if (request.route.plugins.crumb || | ||
!request.route.plugins.hasOwnProperty('crumb') && settings.autoGenerate) { | ||
if (content[request.route.plugins._crumb.key] !== request.plugins.crumb) { | ||
return reply(plugin.hapi.error.forbidden()); | ||
request.route.plugins._crumb = Hoek.applyToDefaults(internals.routeDefaults, request.route.plugins.crumb || {}); | ||
} | ||
else { | ||
request.route.plugins._crumb = false; | ||
} | ||
} | ||
// Remove crumb | ||
// Set crumb cookie and calculate crumb | ||
delete request[request.route.plugins._crumb.source][request.route.plugins._crumb.key]; | ||
} | ||
else { | ||
if (request.method !== 'post' && request.method !== 'put' && request.method !== 'patch' && request.method !== 'delete' || | ||
!request.route.plugins._crumb) { | ||
if ((settings.autoGenerate || | ||
request.route.plugins._crumb) && | ||
(request.server.settings.cors ? internals.originParser(request.headers.origin, settings.allowOrigins) : true)) { | ||
return reply(); | ||
generate(request, reply); | ||
} | ||
var header = request.headers['x-csrf-token']; | ||
// Validate crumb | ||
if (!header) { | ||
return reply(plugin.hapi.error.forbidden()); | ||
if (settings.restful === false || | ||
(!request.route.plugins._crumb || request.route.plugins._crumb.restful === false)) { | ||
if (request.method !== 'post' || | ||
!request.route.plugins._crumb) { | ||
return reply(); | ||
} | ||
var content = request[request.route.plugins._crumb.source]; | ||
if (content instanceof Stream) { | ||
return reply(plugin.hapi.error.forbidden()); | ||
} | ||
if (content[request.route.plugins._crumb.key] !== request.plugins.crumb) { | ||
return reply(plugin.hapi.error.forbidden()); | ||
} | ||
// Remove crumb | ||
delete request[request.route.plugins._crumb.source][request.route.plugins._crumb.key]; | ||
} | ||
else { | ||
if (request.method !== 'post' && request.method !== 'put' && request.method !== 'patch' && request.method !== 'delete' || | ||
!request.route.plugins._crumb) { | ||
if (header !== request.plugins.crumb) { | ||
return reply(plugin.hapi.error.forbidden()); | ||
return reply(); | ||
} | ||
var header = request.headers['x-csrf-token']; | ||
if (!header) { | ||
return reply(plugin.hapi.error.forbidden()); | ||
} | ||
if (header !== request.plugins.crumb) { | ||
return reply(plugin.hapi.error.forbidden()); | ||
} | ||
} | ||
} | ||
return reply(); | ||
}); | ||
return reply(); | ||
}); | ||
plugin.ext('onPreResponse', function (request, reply) { | ||
plugin.ext('onPreResponse', function (request, reply) { | ||
// Add to view context | ||
// Add to view context | ||
var response = request.response; | ||
var response = request.response; | ||
if (settings.addToViewContext && | ||
request.plugins.crumb && | ||
request.route.plugins._crumb && | ||
!response.isBoom && | ||
response.variety === 'view') { | ||
if (settings.addToViewContext && | ||
request.plugins.crumb && | ||
request.route.plugins._crumb && | ||
!response.isBoom && | ||
response.variety === 'view') { | ||
response.source.context = response.source.context || {}; | ||
response.source.context[request.route.plugins._crumb.key] = request.plugins.crumb; | ||
} | ||
response.source.context = response.source.context || {}; | ||
response.source.context[request.route.plugins._crumb.key] = request.plugins.crumb; | ||
} | ||
return reply(); | ||
}); | ||
return reply(); | ||
}); | ||
var generate = function (request, reply) { | ||
var generate = function (request, reply) { | ||
var crumb = request.state[settings.key]; | ||
if (!crumb) { | ||
crumb = Cryptiles.randomString(settings.size); | ||
reply.state(settings.key, crumb, settings.cookieOptions); | ||
} | ||
var crumb = request.state[settings.key]; | ||
if (!crumb) { | ||
crumb = Cryptiles.randomString(settings.size); | ||
reply.state(settings.key, crumb, settings.cookieOptions); | ||
} | ||
request.plugins.crumb = crumb; | ||
return request.plugins.crumb; | ||
}; | ||
request.plugins.crumb = crumb; | ||
return request.plugins.crumb; | ||
}; | ||
plugin.expose({ generate: generate }); | ||
plugin.expose({ generate: generate }); | ||
return next(); | ||
return next(); | ||
}); | ||
}; | ||
@@ -156,0 +200,0 @@ |
{ | ||
"name": "crumb", | ||
"description": "CSRF crumb generation and validation plugin", | ||
"version": "3.1.0", | ||
"author": "Eran Hammer <eran@hueniverse.com> (http://hueniverse.com)", | ||
"version": "3.2.0", | ||
"author": "Eran Hammer <eran@hammer.io> (http://hueniverse.com)", | ||
"contributors": [ | ||
@@ -24,7 +24,8 @@ "Marcus Stong <stongo@gmail.com>", | ||
"engines": { | ||
"node": ">=0.10.22" | ||
"node": ">=0.10.30" | ||
}, | ||
"dependencies": { | ||
"cryptiles": "2.x.x", | ||
"hoek": "2.x.x" | ||
"hoek": "2.x.x", | ||
"joi": "4.x.x" | ||
}, | ||
@@ -31,0 +32,0 @@ "peerDependencies": { |
@@ -1,2 +0,1 @@ | ||
<a href="https://github.com/hapijs"><img src="https://raw.github.com/hapijs/spumko/master/images/from.png" align="right" /></a> | ||
![crumb Logo](https://raw.github.com/hapijs/crumb/master/images/crumb.png) | ||
@@ -19,2 +18,3 @@ | ||
* 'skip' - a function with the signature of function (request reply) {}, which when provided, is called for every request. If the provided function returns true, validation and generation of crumb is skipped (defaults to false) | ||
* 'allowOrigins' - an array of origins to set Crumb cookie on if CORS is enabled. Supports '\*' wildcards for domain segments and port ie '\*.domain.com' or 'domain.com:\*'. '\*' by itself is not allowed | ||
@@ -21,0 +21,0 @@ Additionally, some configuration can be passed on a per-route basis |
@@ -283,2 +283,72 @@ // Load modules | ||
it('does not validate crumb when "skip" option returns true', function (done) { | ||
var server6 = new Hapi.Server(); | ||
server6.route([ | ||
{ | ||
method: 'POST', path: '/1', handler: function (request, reply) { | ||
return reply('test'); | ||
} | ||
} | ||
]); | ||
var skip = function (request, reply) { | ||
return request.headers['x-api-token'] === 'test'; | ||
}; | ||
server6.pack.register({ plugin: require('../'), options: { skip: skip }}, function (err) { | ||
expect(err).to.not.exist; | ||
var headers = {}; | ||
headers['X-API-Token'] = 'test'; | ||
server6.inject({ method: 'POST', url: '/1', headers: headers }, function (res) { | ||
expect(res.statusCode).to.equal(200); | ||
var header = res.headers['set-cookie']; | ||
expect(header).to.not.contain('crumb'); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('ensures crumb validation when "skip" option is not a function', function (done) { | ||
var server6 = new Hapi.Server(); | ||
server6.route([ | ||
{ | ||
method: 'POST', path: '/1', handler: function (request, reply) { | ||
return reply('test'); | ||
} | ||
} | ||
]); | ||
var skip = true; | ||
server6.pack.register({ plugin: require('../'), options: { skip: skip }}, function (err) { | ||
expect(err).to.not.exist; | ||
var headers = {}; | ||
headers['X-API-Token'] = 'not-test'; | ||
server6.inject({ method: 'POST', url: '/1', headers: headers }, function (res) { | ||
expect(res.statusCode).to.equal(403); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('does not allow "*" for allowOrigins setting', function (done) { | ||
var server7 = new Hapi.Server(); | ||
server7.pack.register({ plugin: require('../'), options: { allowOrigins: ['*'] } }, function (err) { | ||
expect(err).to.exist; | ||
done(); | ||
}); | ||
}); | ||
it('does not set crumb cookie insecurely', function(done) { | ||
@@ -304,3 +374,3 @@ var options = { | ||
var header = res.headers['set-cookie']; | ||
expect(header).to.not.contain('crumb'); | ||
expect(header).to.be.undefined; | ||
@@ -312,7 +382,10 @@ done(); | ||
it('does not validate crumb when "skip" option returns true', function (done) { | ||
var server5 = new Hapi.Server(); | ||
it('does set crumb cookie if allowOrigins set and CORS enabled', function(done) { | ||
var options = { | ||
cors: true | ||
} | ||
var server5 = new Hapi.Server(options); | ||
server5.route([ | ||
{ | ||
method: 'POST', path: '/1', handler: function (request, reply) { | ||
method: 'GET', path: '/1', handler: function (request, reply) { | ||
@@ -323,19 +396,69 @@ return reply('test'); | ||
]); | ||
server5.pack.register({ plugin: require('../'), options: { allowOrigins: ['127.0.0.1']} }, function (err) { | ||
expect(err).to.not.exist; | ||
var headers = {}; | ||
headers['Origin'] = '127.0.0.1'; | ||
server5.inject({ method: 'GET', url: '/1', headers: headers }, function (res) { | ||
var skip = function (request, reply) { | ||
var header = res.headers['set-cookie']; | ||
expect(header[0]).to.contain('crumb'); | ||
return request.headers['x-api-token'] === 'test'; | ||
}; | ||
headers['Origin'] = '127.0.0.2'; | ||
server5.pack.register({ plugin: require('../'), options: { skip: skip }}, function (err) { | ||
server5.inject({ method: 'GET', url: '/1', headers: headers }, function (res) { | ||
var header = res.headers['set-cookie']; | ||
expect(header).to.be.undefined; | ||
headers['Origin'] = '127.0.0.1:2000'; | ||
server5.inject({ method: 'GET', url: '/1', headers: headers }, function (res) { | ||
var header = res.headers['set-cookie']; | ||
expect(header).to.be.undefined; | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
it('checks port for allowOrigins setting', function (done) { | ||
var options = { | ||
cors: true | ||
} | ||
var server8 = new Hapi.Server(options); | ||
server8.route([ | ||
{ | ||
method: 'GET', path: '/1', handler: function (request, reply) { | ||
return reply('test'); | ||
} | ||
} | ||
]); | ||
server8.pack.register({ plugin: require('../'), options: { allowOrigins: ['127.0.0.1:2000']} }, function (err) { | ||
expect(err).to.not.exist; | ||
var headers = {}; | ||
headers['X-API-Token'] = 'test'; | ||
server5.inject({ method: 'POST', url: '/1', headers: headers }, function (res) { | ||
headers['Origin'] = '127.0.0.1:2000'; | ||
server8.inject({ method: 'GET', url: '/1', headers: headers }, function (res) { | ||
expect(res.statusCode).to.equal(200); | ||
var header = res.headers['set-cookie']; | ||
expect(header).to.not.contain('crumb'); | ||
expect(header[0]).to.contain('crumb'); | ||
done(); | ||
headers['Origin'] = '127.0.0.1:1000'; | ||
server8.inject({ method: 'GET', url: '/1', headers: headers }, function (res) { | ||
var header = res.headers['set-cookie']; | ||
expect(header).to.be.undefined; | ||
headers['Origin'] = '127.0.0.1'; | ||
server8.inject({ method: 'GET', url: '/1', headers: headers }, function (res) { | ||
var header = res.headers['set-cookie']; | ||
expect(header).to.be.undefined; | ||
done(); | ||
}); | ||
}); | ||
}); | ||
@@ -345,7 +468,10 @@ }); | ||
it('ensures crumb validation when "skip" option is not a function', function (done) { | ||
var server6 = new Hapi.Server(); | ||
server6.route([ | ||
it('parses wildcards in allowOrigins setting', function (done) { | ||
var options = { | ||
cors: true | ||
} | ||
var server9 = new Hapi.Server(options); | ||
server9.route([ | ||
{ | ||
method: 'POST', path: '/1', handler: function (request, reply) { | ||
method: 'GET', path: '/1', handler: function (request, reply) { | ||
@@ -356,14 +482,28 @@ return reply('test'); | ||
]); | ||
var skip = true; | ||
server6.pack.register({ plugin: require('../'), options: { skip: skip }}, function (err) { | ||
server9.pack.register({ plugin: require('../'), options: { allowOrigins: ['127.0.0.1:*', '*.test.com']} }, function (err) { | ||
expect(err).to.not.exist; | ||
var headers = {}; | ||
headers['X-API-Token'] = 'not-test'; | ||
server6.inject({ method: 'POST', url: '/1', headers: headers }, function (res) { | ||
headers['Origin'] = '127.0.0.1:2000'; | ||
server9.inject({ method: 'GET', url: '/1', headers: headers }, function (res) { | ||
expect(res.statusCode).to.equal(403); | ||
var header = res.headers['set-cookie']; | ||
expect(header[0]).to.contain('crumb'); | ||
done(); | ||
headers['Origin'] = 'foo.test.com'; | ||
server9.inject({ method: 'GET', url: '/1', headers: headers }, function (res) { | ||
//expect(header[0]).to.contain('crumb'); | ||
expect(header[0]).to.contain('crumb'); | ||
headers['Origin'] = 'foo.tesc.com'; | ||
server9.inject({ method: 'GET', url: '/1', headers: headers }, function (res) { | ||
var header = res.headers['set-cookie']; | ||
expect(header).to.be.undefined; | ||
done(); | ||
}); | ||
}); | ||
}); | ||
@@ -370,0 +510,0 @@ }); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
105566
19
719
4
+ Addedjoi@4.x.x
+ Addedisemail@1.2.0(transitive)
+ Addedjoi@4.9.0(transitive)
+ Addedmoment@2.30.1(transitive)
+ Addedtopo@1.1.0(transitive)