Skip to content

Commit

Permalink
Merge pull request #70 from hapijs/hapi12
Browse files Browse the repository at this point in the history
Hapi12
  • Loading branch information
stongo committed Jan 15, 2016
2 parents 44e49ea + 3d34f5c commit 34b1103
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 518 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
language: node_js

node_js:
- 0.10
- 4
- 4.0
- 5
9 changes: 1 addition & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,7 @@ Lead Maintainer: [Marcus Stong](https://github.com/stongo)

Crumb has been refactored to securely work with CORS, as [OWASP](https://www.owasp.org/index.php/HTML5_Security_Cheat_Sheet#Cross_Origin_Resource_Sharing) recommends using CSRF protection with CORS.

The `allowOrigins` option allows you to have fine grained control on which Cross Origin sites get the Crumb cookie set. This is useful for APIs that have some consumers only using GET routes (no Crumb token should be set) while other consumers have permission for POST/PUT/PATCH/DELETE routes.

If the `allowOrigins` setting is not set, the server's `cors.origin` list will be used to determine when to set the Crumb cookie on Cross Origin requests.

To use Crumb securely on a server that allows Same Origin requests and CORS, it's a requirement to set server `host` to a hostname rather than an IP for Crumb to determine same origin requests. If you use an IP as the server host, your Same Origin requests will not get the Crumb cookie set.

**Note that Crumb will not work with `allowOrigins` or `cors.origin` set to "\*"**
**It is highly discouraged to have a production servers `cors.origin` setting set to "[\*]" or "true" with Crumb as it will leak the crumb token to potentially malicious sites**

## Plugin Options

Expand All @@ -29,7 +23,6 @@ The following options are available when registering the plugin
* 'cookieOptions' - storage options for the cookie containing the crumb, see the [server.state](http://hapijs.com/api#serverstatename-options) documentation of hapi for more information
* 'restful' - RESTful mode that validates crumb tokens from "X-CSRF-Token" request header for POST, PUT, PATCH and DELETE server routes. Disables payload/query crumb validation (defaults to false)
* '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. Defaults to the server's `cors.origin` setting by default

Additionally, some configuration can be passed on a per-route basis

Expand Down
10 changes: 6 additions & 4 deletions example/restful.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
var Hapi = require('hapi');
'use strict';

var server = new Hapi.Server();
const Hapi = require('hapi');

const server = new Hapi.Server();
server.connection({ host: '127.0.0.1', port: 8000 });

// Add Crumb plugin

server.register({ register: require('../'), options: { restful: true } }, function (err) {
server.register({ register: require('../'), options: { restful: true } }, (err) => {

if (err) {
throw err;
Expand Down Expand Up @@ -38,7 +40,7 @@ server.route([
}
]);

server.start(function () {
server.start(() => {

console.log('Example restful server running at:', server.info.uri);
});
10 changes: 6 additions & 4 deletions example/server.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
var Hapi = require('hapi');
'use strict';

var server = new Hapi.Server();
const Hapi = require('hapi');

const server = new Hapi.Server();
server.connection({ host: '127.0.0.1', port: 8000 });

server.views({
Expand All @@ -10,7 +12,7 @@ server.views({
}
});

server.register({ register: require('../'), options: { cookieOptions: { isSecure: false } } }, function (err) {
server.register({ register: require('../'), options: { cookieOptions: { isSecure: false } } }, (err) => {

if (err) {
throw err;
Expand All @@ -35,7 +37,7 @@ server.route({
}
});

server.start(function () {
server.start(() => {

console.log('Example server running at:', server.info.uri);
});
117 changes: 21 additions & 96 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
'use strict';

// Load modules

var Stream = require('stream');
var Boom = require('boom');
var Cryptiles = require('cryptiles');
var Hoek = require('hoek');
var Joi = require('joi');
const Stream = require('stream');
const Boom = require('boom');
const Cryptiles = require('cryptiles');
const Hoek = require('hoek');
const Joi = require('joi');


// Declare internals

var internals = {};
const internals = {};


internals.schema = Joi.object().keys({
Expand All @@ -19,8 +21,7 @@ internals.schema = Joi.object().keys({
addToViewContext: Joi.boolean().optional(),
cookieOptions: Joi.object().keys(null),
restful: Joi.boolean().optional(),
skip: Joi.func().optional(),
allowOrigins: Joi.array().items(Joi.string().valid('*').forbidden()).optional()
skip: Joi.func().optional()
});


Expand All @@ -33,29 +34,28 @@ internals.defaults = {
path: '/'
},
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
allowOrigins: null // A list of CORS origins to set crumb cookie on. Defaults to request.route.settings.settings.cors.origin
skip: false // Set to a function which returns true when to skip crumb generation and validation
};


exports.register = function (server, options, next) {

var validateOptions = internals.schema.validate(options);
const validateOptions = internals.schema.validate(options);
if (validateOptions.error) {
return next(validateOptions.error);
}

var settings = Hoek.applyToDefaults(internals.defaults, options);
const settings = Hoek.applyToDefaults(internals.defaults, options);

var routeDefaults = {
const routeDefaults = {
key: settings.key,
restful: settings.restful,
source: 'payload'
};

server.state(settings.key, settings.cookieOptions);

server.ext('onPostAuth', function (request, reply) {
server.ext('onPostAuth', (request, reply) => {

// If skip function enabled. Call it and if returns true, do not attempt to do anything with crumb.

Expand All @@ -80,7 +80,7 @@ exports.register = function (server, options, next) {

if ((settings.autoGenerate ||
request.route.settings.plugins._crumb) &&
(request.route.settings.cors ? internals.originParser(request.headers.origin, settings.allowOrigins || request.route.settings.cors.origin, request) : true)) {
(request.route.settings.cors ? request.info.cors.isOriginMatch : true)) {

generate(request, reply);
}
Expand All @@ -96,7 +96,7 @@ exports.register = function (server, options, next) {
return reply.continue();
}

var content = request[request.route.settings.plugins._crumb.source];
const content = request[request.route.settings.plugins._crumb.source];
if (!content || content instanceof Stream) {

return reply(Boom.forbidden());
Expand All @@ -117,7 +117,7 @@ exports.register = function (server, options, next) {
return reply.continue();
}

var header = request.headers['x-csrf-token'];
const header = request.headers['x-csrf-token'];

if (!header) {
return reply(Boom.forbidden());
Expand All @@ -132,11 +132,11 @@ exports.register = function (server, options, next) {
return reply.continue();
});

server.ext('onPreResponse', function (request, reply) {
server.ext('onPreResponse', (request, reply) => {

// Add to view context

var response = request.response;
const response = request.response;

if (settings.addToViewContext &&
request.plugins.crumb &&
Expand All @@ -151,9 +151,9 @@ exports.register = function (server, options, next) {
return reply.continue();
});

var generate = function (request, reply) {
const generate = function (request, reply) {

var crumb = request.state[settings.key];
let crumb = request.state[settings.key];
if (!crumb) {
crumb = Cryptiles.randomString(settings.size);
reply.state(settings.key, crumb, settings.cookieOptions);
Expand All @@ -171,78 +171,3 @@ exports.register = function (server, options, next) {
exports.register.attributes = {
pkg: require('../package.json')
};


// Strip http or https from request host

internals.trimHost = function (host) {

this._host = host;

if (host.indexOf('https://') === 0) {
this._host = this._host.substring(8);
}
if (host.indexOf('http://') === 0) {
this._host = this._host.substring(7);
}

return this._host;
};


// Parses allowOrigin setting

internals.originParser = function (origin, allowOrigins, request) {

var host = internals.trimHost(request.connection.info.uri);
var requestHost = request.headers.host;
this._match = false;

// If a same origin request, pass check

if (host === requestHost) {
this._match = true;
return this._match;
}

// If no origin header is set and cross-origin, automatically fail check

else if (!origin || allowOrigins.length === 0) {
return this._match;
}

// Split origin in to port and domain

this._origin = origin.split(':');
this._originPort = this._origin.length === 3 ? this._origin[2] : null;
this._originParts = this._origin[1].split('.');

// Iterate through allowed origins list and check origin header matches

for (var i = 0, il = allowOrigins.length; i < il; ++i) {
if (allowOrigins[i] === '*') {
return false;
}

this._originAllow = allowOrigins[i].split(':');
this._originAllowPort = this._originAllow.length === 3 ? this._originAllow[2] : null;
this._originAllowParts = this._originAllow[1].split('.');

if ((this._originPort && !this._originAllowPort) || (!this._originPort && this._originAllowPort) || (this._originAllowPort !== '*' && this._originPort !== this._originAllowPort)) {
this._match = false;
}
else {
for (var j = 0, jl = this._originAllowParts.length; j < jl; ++j) {
this._match = this._originAllowParts[j] === '//*' || this._originAllowParts[j] === this._originParts[j];
if (!this._match) {
break;
}
}
if (this._match) {
return this._match;
}
}
}

return this._match;
};
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "crumb",
"description": "CSRF crumb generation and validation plugin",
"version": "5.0.0",
"version": "6.0.0",
"repository": "git://github.com/hapijs/crumb",
"bugs": {
"url": "https://github.com/hapijs/crumb/issues"
Expand All @@ -15,23 +15,23 @@
"session"
],
"engines": {
"node": ">=0.10.32"
"node": ">=4.0.0"
},
"dependencies": {
"boom": "2.x.x",
"cryptiles": "2.x.x",
"hoek": "2.x.x",
"joi": "6.x.x"
"boom": "3.x.x",
"cryptiles": "3.x.x",
"hoek": "3.x.x",
"joi": "7.x.x"
},
"peerDependencies": {
"hapi": ">=8.x.x"
"hapi": ">=12.x.x"
},
"devDependencies": {
"code": "1.x.x",
"handlebars": "1.3.x",
"hapi": "9.x.x",
"lab": "6.x.x",
"vision": "^3.0.0"
"code": "2.x.x",
"handlebars": "^4.0.5",
"hapi": "12.x.x",
"lab": "8.x.x",
"vision": "^4.0.0"
},
"scripts": {
"test": "lab -r console -t 100 -a code -L",
Expand Down
Loading

0 comments on commit 34b1103

Please sign in to comment.