Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

crumb

Package Overview
Dependencies
Maintainers
2
Versions
46
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

crumb - npm Package Compare versions

Comparing version 3.1.0 to 3.2.0

CONTRIBUTING.md

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 @@ });

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc