Skip to content

Commit

Permalink
Enabled CSRF protection by default, tests not passing
Browse files Browse the repository at this point in the history
  • Loading branch information
avoidwork committed Jan 10, 2015
1 parent 9509cf7 commit e4addfe
Show file tree
Hide file tree
Showing 15 changed files with 180 additions and 134 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Change Log

## 1.0.2
- Fixed `X-CSRF-Token` header decoration on response, such that it's only applied to protected routes
## 1.1.0
- Upgraded turtle.io for required fix
- Fixed `X-CSRFToken` header decoration on response, such that it's only applied to protected routes
- Enabled `CSRF` protection by default

## 1.0.1
- Upgraded turtle.io for recommended fixes
Expand Down
1 change: 1 addition & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = function (grunt) {
dist : {
src : [
"src/intro.js",
"src/regex.js",
"src/constructor.js",
"src/auth.js",
"src/bootstrap.js",
Expand Down
3 changes: 2 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
},
"security": {
"key": "x-csrf-token",
"csrf": false,
"secret": "tenso",
"csrf": true,
"csp": null,
"xframe": "",
"p3p": "",
Expand Down
130 changes: 69 additions & 61 deletions lib/tenso.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,61 @@
* @license BSD-3 <https://raw.github.com/avoidwork/tenso/master/LICENSE>
* @link http://avoidwork.github.io/tenso
* @module tenso
* @version 1.0.2
* @version 1.1.0
*/
( function () {
"use strict";

var turtleio = require( "turtle.io" ),
SERVER = "tenso/1.0.2",
CONFIG = require( __dirname + "/../config.json" ),
keigai = require( "keigai" ),
util = keigai.util,
array = util.array,
clone = util.clone,
coerce = util.coerce,
iterate = util.iterate,
json = util.json,
merge = util.merge,
string = util.string,
uuid = util.uuid,
session = require( "express-session" ),
cookie = require( "cookie-parser" ),
lusca = require( "lusca" ),
passport = require( "passport" ),
BasicStrategy = require( "passport-http" ).BasicStrategy,
BearerStrategy = require( "passport-http-bearer" ).Strategy,
FacebookStrategy = require( "passport-facebook" ).Strategy,
GoogleStrategy = require( "passport-google" ).Strategy,
LinkedInStrategy = require( "passport-linkedin" ).Strategy,
LocalStrategy = require( "passport-local" ).Strategy,
OAuth2Strategy = require( "passport-oauth2" ).Strategy,
SAMLStrategy = require( "passport-saml" ).Strategy,
TwitterStrategy = require( "passport-twitter" ).Strategy,
RedisStore = require( "connect-redis" )( session ),
REGEX_HYPERMEDIA = /[a-zA-Z]+_(guid|uuid|id|url|uri)$/,
REGEX_TRAILING = /_.*$/,
REGEX_TRAILING_S = /s$/,
REGEX_TRAILING_Y = /y$/,
REGEX_SCHEME = /^(\w+\:\/\/)|\//,
REGEX_COLLECTION = /(.*)(\/.*)$/,
REGEX_MODIFY = /DELETE|PATCH|POST|PUT/,
REGEX_GETREWRITE = /HEAD|OPTIONS/i,
REGEX_BODY = /POST|PUT|PATCH/i,
REGEX_FORMENC = /application\/x-www-form-urlencoded/,
REGEX_JSONENC = /application\/json/,
REGEX_BODY_SPLIT = /&|=/,
REGEX_LEADING = /.*\//,
REGEX_ID = /^(_id|id)$/i,
REGEX_TRAIL_SLASH = /\/$/;
"use strict";

var CONFIG = require( __dirname + "/../config.json" ),
SERVER = "tenso/1.1.0",
keigai = require( "keigai" ),
util = keigai.util,
array = util.array,
clone = util.clone,
coerce = util.coerce,
iterate = util.iterate,
json = util.json,
merge = util.merge,
string = util.string,
uuid = util.uuid,
turtleio = require( "turtle.io" ),
session = require( "express-session" ),
cookie = require( "cookie-parser" ),
lusca = require( "lusca" ),
passport = require( "passport" ),
BasicStrategy = require( "passport-http" ).BasicStrategy,
BearerStrategy = require( "passport-http-bearer" ).Strategy,
FacebookStrategy = require( "passport-facebook" ).Strategy,
GoogleStrategy = require( "passport-google" ).Strategy,
LinkedInStrategy = require( "passport-linkedin" ).Strategy,
LocalStrategy = require( "passport-local" ).Strategy,
OAuth2Strategy = require( "passport-oauth2" ).Strategy,
SAMLStrategy = require( "passport-saml" ).Strategy,
TwitterStrategy = require( "passport-twitter" ).Strategy,
RedisStore = require( "connect-redis" )( session );

/**
* RegExp cache
*
* @type Object
*/
var regex = {
body: /POST|PUT|PATCH/i,
body_split: /&|=/,
collection: /(.*)(\/.*)$/,
encode_form: /application\/x-www-form-urlencoded/,
encode_json: /application\/json/,
get_rewrite: /HEAD|OPTIONS/i,
hypermedia: /[a-zA-Z]+_(guid|uuid|id|url|uri)$/,
id: /^(_id|id)$/i,
leading: /.*\//,
modify: /DELETE|PATCH|POST|PUT/,
scheme: /^(\w+\:\/\/)|\//,
trailing: /_.*$/,
trailing_s: /s$/,
trailing_slash: /\/$/,
trailing_y: /y$/
};

/**
* Tenso
Expand All @@ -65,7 +73,7 @@ function Tenso () {
this.rates = {};
this.server = turtleio();
this.server.tenso = this;
this.version = "1.0.2";
this.version = "1.1.0";
}

/**
Expand Down Expand Up @@ -184,7 +192,7 @@ Tenso.prototype.respond = function ( req, res, arg, status, headers ) {
}
}

if ( REGEX_MODIFY.test( req.allow ) && this.server.config.security.csrf && res.locals[ this.server.config.security.key ] ) {
if ( !regex.modify.test( req.method ) && regex.modify.test( req.allow ) && this.server.config.security.csrf && res.locals[ this.server.config.security.key ] ) {
ref[ 0 ][ this.server.config.security.key ] = res.locals[ this.server.config.security.key ];
}

Expand Down Expand Up @@ -282,7 +290,7 @@ function auth ( obj, config ) {
obj.server.use( fnCookie ).blacklist( fnCookie );

if ( config.security.csrf ) {
luscaCsrf = lusca.csrf( { key: config.security.key } );
luscaCsrf = lusca.csrf( { key: config.security.key, secret: config.security.secret } );
obj.server.use( luscaCsrf ).blacklist( luscaCsrf );
}
}
Expand Down Expand Up @@ -671,19 +679,19 @@ function bootstrap ( obj, config ) {
function parse ( req ) {
var args, type;

if ( REGEX_BODY.test( req.method ) && req.body !== undefined ) {
if ( regex.body.test( req.method ) && req.body !== undefined ) {
type = req.headers[ "content-type" ];

if ( REGEX_FORMENC.test( type ) ) {
args = req.body ? array.chunk( req.body.split( REGEX_BODY_SPLIT ), 2 ) : [];
if ( regex.encode_form.test( type ) ) {
args = req.body ? array.chunk( req.body.split( regex.body_split ), 2 ) : [];
req.body = {};

array.each( args, function ( i ) {
req.body[ i[ 0 ] ] = coerce( i[ 1 ] );
} );
}

if ( REGEX_JSONENC.test( type ) ) {
if ( regex.encode_json.test( type ) ) {
req.body = json.decode( req.body, true );
}
}
Expand Down Expand Up @@ -812,17 +820,17 @@ function hypermedia ( server, req, rep, headers ) {
var collection, uri;

// If ID like keys are found, and are not URIs, they are assumed to be root collections
if ( REGEX_ID.test( i ) || REGEX_HYPERMEDIA.test( i ) ) {
if ( !REGEX_ID.test( i ) ) {
collection = i.replace( REGEX_TRAILING, "" ).replace( REGEX_TRAILING_S, "" ).replace( REGEX_TRAILING_Y, "ie" ) + "s";
if ( regex.id.test( i ) || regex.hypermedia.test( i ) ) {
if ( !regex.id.test( i ) ) {
collection = i.replace( regex.trailing, "" ).replace( regex.trailing_s, "" ).replace( regex.trailing_y, "ie" ) + "s";
rel = "related";
}
else {
collection = item_collection;
rel = "item";
}

uri = REGEX_SCHEME.test( obj[ i ] ) ? ( obj[ i ].indexOf( "//" ) > -1 ? obj[ i ] : protocol + "//" + req.parsed.host + obj[ i ] ) : ( protocol + "//" + req.parsed.host + "/" + collection + "/" + obj[ i ] );
uri = regex.scheme.test( obj[ i ] ) ? ( obj[ i ].indexOf( "//" ) > -1 ? obj[ i ] : protocol + "//" + req.parsed.host + obj[ i ] ) : ( protocol + "//" + req.parsed.host + "/" + collection + "/" + obj[ i ] );

if ( uri !== root && !seen[ uri ] ) {
rep.data.link.push( { uri: uri, rel: rel } );
Expand All @@ -844,7 +852,7 @@ function hypermedia ( server, req, rep, headers ) {

if ( req.parsed.pathname !== "/" ) {
rep.data.link.push( {
uri: root.replace( REGEX_TRAIL_SLASH, "" ).replace( REGEX_COLLECTION, "$1" ),
uri: root.replace( regex.trailing_slash, "" ).replace( regex.collection, "$1" ),
rel: "collection"
} );
}
Expand Down Expand Up @@ -890,7 +898,7 @@ function hypermedia ( server, req, rep, headers ) {
array.each( rep.data.result, function ( i ) {
var uri;

if ( typeof i == "string" && REGEX_SCHEME.test( i ) ) {
if ( typeof i == "string" && regex.scheme.test( i ) ) {
uri = i.indexOf( "//" ) > -1 ? i : protocol + "//" + req.parsed.host + i;

if ( uri !== root ) {
Expand All @@ -899,7 +907,7 @@ function hypermedia ( server, req, rep, headers ) {
}

if ( i instanceof Object ) {
parse( i, "item", req.parsed.pathname.replace( REGEX_TRAIL_SLASH, "" ).replace( REGEX_LEADING, "" ) );
parse( i, "item", req.parsed.pathname.replace( regex.trailing_slash, "" ).replace( regex.leading, "" ) );
}
} );
}
Expand Down Expand Up @@ -940,7 +948,7 @@ function keymaster ( req, res, next ) {

// No authentication, or it's already happened
if ( !req.protect || !req.protectAsync || ( req.session && req.isAuthenticated() ) ) {
method = REGEX_GETREWRITE.test( req.method ) ? "get" : req.method.toLowerCase();
method = regex.get_rewrite.test( req.method ) ? "get" : req.method.toLowerCase();
routes = req.server.config.routes[ method ] || {};
uri = req.parsed.pathname;
valid = false;
Expand Down Expand Up @@ -1019,7 +1027,7 @@ function prepare ( arg, error, status ) {

return {
data: data || null,
error: error ? ( error.stack || error.message || error ) : null,
error: error ? ( error.message || error ) : null,
status: status || 200
};
}
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "tenso",
"description": "Tensō is a REST API facade for node.js, designed to simplify the implementation of APIs.",
"version": "1.0.2",
"version": "1.1.0",
"homepage": "http://avoidwork.github.io/tenso",
"author": {
"name": "Jason Mulligan",
Expand Down Expand Up @@ -29,7 +29,7 @@
},
"dependencies": {
"keigai": "~1.0.0",
"turtle.io": "~3.1.16",
"turtle.io": "~3.1.19",
"cookie-parser": "~1.3.3",
"express-session": "~1.8.2",
"passport": "~0.2.0",
Expand All @@ -56,14 +56,14 @@
"grunt-jsdoc": "~0.5.6",
"ink-docstrap": "~0.4.12",
"hippie": "~0.3.0",
"chai": "^1.9.1",
"grunt-mocha-test": "^0.11.0",
"grunt-nsp-package": "^0.0.5"
},
"keywords": [
"REST",
"API",
"facade",
"server"
"server",
"hypermedia"
]
}
9 changes: 8 additions & 1 deletion routes.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
var uuid = require( "keigai" ).util.uuid;

module.exports.get = {
"/": ["/reports", "/uuid"],
"/": ["/login", "/reports", "/uuid"],
"/login": "POST to login",
"/reports": ["/reports/tps"],
"/reports/tps": function ( req, res ) {
res.error( 785, new Error( "TPS Cover Sheet not attached" ) );
},
"/uuid": function ( req, res ) {
res.respond( uuid(), 200, {"cache-control": "no-cache"} );
}
}

module.exports.post = {
"/login" : function ( req, res ) {
res.respond( uuid(), 200, {"cache-control": "no-cache"} );
}
}
2 changes: 1 addition & 1 deletion src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function auth ( obj, config ) {
obj.server.use( fnCookie ).blacklist( fnCookie );

if ( config.security.csrf ) {
luscaCsrf = lusca.csrf( { key: config.security.key } );
luscaCsrf = lusca.csrf( { key: config.security.key, secret: config.security.secret } );
obj.server.use( luscaCsrf ).blacklist( luscaCsrf );
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ function bootstrap ( obj, config ) {
function parse ( req ) {
var args, type;

if ( REGEX_BODY.test( req.method ) && req.body !== undefined ) {
if ( regex.body.test( req.method ) && req.body !== undefined ) {
type = req.headers[ "content-type" ];

if ( REGEX_FORMENC.test( type ) ) {
args = req.body ? array.chunk( req.body.split( REGEX_BODY_SPLIT ), 2 ) : [];
if ( regex.encode_form.test( type ) ) {
args = req.body ? array.chunk( req.body.split( regex.body_split ), 2 ) : [];
req.body = {};

array.each( args, function ( i ) {
req.body[ i[ 0 ] ] = coerce( i[ 1 ] );
} );
}

if ( REGEX_JSONENC.test( type ) ) {
if ( regex.encode_json.test( type ) ) {
req.body = json.decode( req.body, true );
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Tenso.prototype.respond = function ( req, res, arg, status, headers ) {
}
}

if ( REGEX_MODIFY.test( req.allow ) && this.server.config.security.csrf && res.locals[ this.server.config.security.key ] ) {
if ( !regex.modify.test( req.method ) && regex.modify.test( req.allow ) && this.server.config.security.csrf && res.locals[ this.server.config.security.key ] ) {
ref[ 0 ][ this.server.config.security.key ] = res.locals[ this.server.config.security.key ];
}

Expand Down
Loading

0 comments on commit e4addfe

Please sign in to comment.