diff --git a/lib/hooks/csrf/index.js b/lib/hooks/csrf/index.js index 03317e7b1..702d3c24a 100644 --- a/lib/hooks/csrf/index.js +++ b/lib/hooks/csrf/index.js @@ -5,7 +5,8 @@ module.exports = function(sails) { */ var _ = require('lodash'), - util = require('sails-util'); + util = require('sails-util'), + pathToRegexp = require('path-to-regexp'); /** * Expose hook definition @@ -69,6 +70,27 @@ module.exports = function(sails) { // Quick trim function--could move this into sails.util at some point function trim (str) {return str.trim();} + var disabledRoutes; + if (Array.isArray(sails.config.csrf.routesDisabled)) { + disabledRoutes = sails.config.csrf.routesDisabled; + } else if (_.isRegExp(sails.config.csrf.routesDisabled)) { + disabledRoutes = [sails.config.csrf.routesDisabled]; + } else { + disabledRoutes = sails.config.csrf.routesDisabled.split(',').map(trim); + } + + var disabledRouteCheckers = disabledRoutes.map(function(route) { + var parsedRegexp; + if (_.isString(route)) { + parsedRegexp = pathToRegexp(route, []); + return function(req) { return parsedRegexp.exec(req.path) }; + } else if (_.isRegExp(route)) { + return function(req) { return route.test(req.path) }; + } else { + return function() { return false }; + } + }); + // Add res.view() method to compatible middleware sails.on('router:before', function () { @@ -76,7 +98,7 @@ module.exports = function(sails) { var allowCrossOriginCSRF = sails.config.csrf.origin.split(',').map(trim).indexOf(req.headers.origin) > -1; - var isRouteDisabled = sails.config.csrf.routesDisabled.split(',').map(trim).indexOf(req.path) > -1; + var isRouteDisabled = disabledRouteCheckers.some(function(checker) { return checker(req); }) // Start with a clear _csrf template token res.locals._csrf = null; diff --git a/package.json b/package.json index bca759acd..a9ef1df1d 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "mock-req": "0.2.0", "mock-res": "0.3.0", "parseurl": "1.3.1", + "path-to-regexp": "1.2.1", "pluralize": "1.2.1", "prompt": "0.2.14", "rc": "1.0.1", diff --git a/test/integration/hook.cors_csrf.test.js b/test/integration/hook.cors_csrf.test.js index f3f3be21b..1f32dea25 100644 --- a/test/integration/hook.cors_csrf.test.js +++ b/test/integration/hook.cors_csrf.test.js @@ -1101,10 +1101,10 @@ describe('CORS and CSRF ::', function() { }); - describe("with CSRF set to {protectionEnabled: true, routesDisabled: '/foo, /user'}", function() { + describe("with CSRF set to {protectionEnabled: true, routesDisabled: '/foo/:id, /user'}", function() { before(function() { - fs.writeFileSync(path.resolve('../', appName, 'config/csrf.js'), "module.exports.csrf = {protectionEnabled: true, routesDisabled: '/user'};"); + fs.writeFileSync(path.resolve('../', appName, 'config/csrf.js'), "module.exports.csrf = {protectionEnabled: true, routesDisabled: '/foo/:id, /user'};"); }); it("a POST request on /user without a CSRF token should result in a 201 response", function(done) { @@ -1115,7 +1115,16 @@ describe('CORS and CSRF ::', function() { assert.equal(response.statusCode, 201); done(); }); + }); + it("a POST request on /foo/12 without a CSRF token should result in a 404 response", function(done) { + httpHelper.testRoute("post", 'foo/12', function(err, response) { + if (err) { + return done(err); + } + assert.equal(response.statusCode, 404); + done(); + }); }); it("a POST request on /test without a CSRF token should result in a 403 response", function(done) { @@ -1126,7 +1135,132 @@ describe('CORS and CSRF ::', function() { assert.equal(response.statusCode, 403); done(); }); + }); + + it("a POST request on /foo without a CSRF token should result in a 403 response", function(done) { + httpHelper.testRoute("post", 'foo', function(err, response) { + if (err) { + return done(err); + } + assert.equal(response.statusCode, 403); + done(); + }); + }); + + }); + + describe("with CSRF set to {protectionEnabled: true, routesDisabled: /user\\/\\d+/}", function() { + + before(function() { + fs.writeFileSync(path.resolve('../', appName, 'config/csrf.js'), "module.exports.csrf = {protectionEnabled: true, routesDisabled: /user\\/\\d+/};"); + }); + + it("a POST request on /user/1 without a CSRF token should result in a 200 response", function(done) { + httpHelper.testRoute("post", 'user/1', function(err, response) { + if (err) { + return done(err); + } + assert.equal(response.statusCode, 200); + done(); + }); + }); + + it("a POST request on /user/a without a CSRF token should result in a 403 response", function(done) { + httpHelper.testRoute("post", 'user/a', function(err, response) { + if (err) { + return done(err); + } + assert.equal(response.statusCode, 403); + done(); + }); + }); + + it("a POST request on /user without a CSRF token should result in a 403 response", function(done) { + httpHelper.testRoute("post", 'user', function(err, response) { + if (err) { + return done(err); + } + assert.equal(response.statusCode, 403); + done(); + }); + }); + + }); + + describe("with CSRF set to {protectionEnabled: true, routesDisabled: ['/foo/:id', '/bar/foo', /user\\/\\d+/]}", function() { + + before(function() { + fs.writeFileSync(path.resolve('../', appName, 'config/csrf.js'), "module.exports.csrf = {protectionEnabled: true, routesDisabled: ['/foo/:id', '/bar/foo', /user\\/\\d+/]};"); + }); + + it("a POST request on /foo/12 without a CSRF token should result in a 404 response", function(done) { + httpHelper.testRoute("post", 'foo/12', function(err, response) { + if (err) { + return done(err); + } + assert.equal(response.statusCode, 404); + done(); + }); + }); + + it("a POST request on /bar/foo without a CSRF token should result in a 404 response", function(done) { + httpHelper.testRoute("post", 'bar/foo', function(err, response) { + if (err) { + return done(err); + } + assert.equal(response.statusCode, 404); + done(); + }); + }); + + it("a POST request on /user/1 without a CSRF token should result in a 200 response", function(done) { + httpHelper.testRoute("post", 'user/1', function(err, response) { + if (err) { + return done(err); + } + assert.equal(response.statusCode, 200); + done(); + }); + }); + + it("a POST request on /user/a without a CSRF token should result in a 403 response", function(done) { + httpHelper.testRoute("post", 'user/a', function(err, response) { + if (err) { + return done(err); + } + assert.equal(response.statusCode, 403); + done(); + }); + }); + + it("a POST request on /foo without a CSRF token should result in a 403 response", function(done) { + httpHelper.testRoute("post", 'foo', function(err, response) { + if (err) { + return done(err); + } + assert.equal(response.statusCode, 403); + done(); + }); + }); + it("a POST request on /user without a CSRF token should result in a 403 response", function(done) { + httpHelper.testRoute("post", 'user', function(err, response) { + if (err) { + return done(err); + } + assert.equal(response.statusCode, 403); + done(); + }); + }); + + it("a POST request on /test without a CSRF token should result in a 403 response", function(done) { + httpHelper.testRoute("post", 'test', function(err, response) { + if (err) { + return done(err); + } + assert.equal(response.statusCode, 403); + done(); + }); }); });