├── .gitignore ├── .npmignore ├── History.md ├── Makefile ├── Readme.md ├── index.js ├── package.json └── test ├── csrf.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .npm-debug.log 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | support 2 | test 3 | examples 4 | *.sock 5 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 1.0.0 / 2010-01-03 3 | ================== 4 | 5 | * Initial release 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @./node_modules/.bin/mocha \ 4 | --harmony 5 | 6 | .PHONY: test 7 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # stateless-csrf 3 | 4 | CSRF without sessions. 5 | 6 | ## Installation 7 | 8 | npm install stateless-csrf 9 | 10 | ## How it works 11 | 12 | This CSRF protection hashes a user's unique cookie against a server-side secret. 13 | 14 | When the request comes in, the server hashes the cookie with the server-side 15 | secret and then compares it to the CSRF token. If it matches, verification is complete, 16 | otherwise the middleware rejects the request. 17 | 18 | This is a slight variation on the [double submit cookies](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Double_Submit_Cookies) using the advice 19 | mentioned in [this comment](http://discourse.codinghorror.com/t/preventing-csrf-and-xsrf-attacks/268/61) in [this blog post](http://blog.codinghorror.com/preventing-csrf-and-xsrf-attacks/). 20 | 21 | ## Usage 22 | 23 | ```js 24 | var csrf = require('stateless-csrf') 25 | 26 | app.use(csrf({ 27 | secret: 'some server secret', 28 | cookie: 'name of the cookie to hash against' 29 | })) 30 | 31 | app.use(function * (next) { 32 | if ('GET' == this.method) { 33 | this.body = this.state.csrf 34 | } else if ('POST' == this.method) { 35 | this.body = 'protected area'; 36 | } 37 | }) 38 | ``` 39 | 40 | ## Test 41 | 42 | ``` 43 | npm install 44 | make test 45 | ``` 46 | 47 | ## Considerations 48 | 49 | - **Add a salt**: not sure if this is necessary since the user token is already unique. 50 | - **Add an expiration**: not sure this is necessary since the cookie has an expiration. 51 | 52 | ## Disclaimer 53 | 54 | I am not a security expert nor have I done a security audit on this code. 55 | 56 | Use this at your own risk, and if you can think of any ways to make this more secure, let me know! 57 | 58 | ## License 59 | 60 | MIT 61 | 62 | Copyright (c) 2015 Matthew Mueller <mattmuelle@gmail.com> 63 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var assign = require('object-assign') 6 | var assert = require('assert') 7 | var hasha = require('hasha') 8 | 9 | /** 10 | * Export `csrf` 11 | */ 12 | 13 | module.exports = csrf 14 | 15 | /** 16 | * Ignore 17 | */ 18 | 19 | var ignore = { 20 | 'HEAD': true, 21 | 'OPTIONS': true 22 | } 23 | 24 | /** 25 | * Initialize `csrf` 26 | * 27 | * @param {Object} options 28 | * @return {Generator} 29 | */ 30 | 31 | function csrf (options) { 32 | options = options || {} 33 | 34 | assert(options.secret, 'you must pass a server-side secret to use in the encryption') 35 | assert(options.cookie, 'you must pass the key of the cookie to generate the token and verify authenticity') 36 | 37 | return function *_csrf (next) { 38 | if (ignore[this.method]) return yield next 39 | 40 | // set the default based on if the keygrip is set or not. 41 | options.sign = options.sign === undefined 42 | ? this.app.keys && this.app.keys.length 43 | : !!options.sign 44 | 45 | // generate a CSRF token 46 | return 'GET' == this.method 47 | ? yield* create.call(this, options, next) 48 | : yield* verify.call(this, options, next) 49 | } 50 | } 51 | 52 | /** 53 | * Generate a CSRF token 54 | * 55 | * @param {Object} options 56 | * @param {Generator} next 57 | */ 58 | 59 | function *create (options, next) { 60 | var token = this.cookies.get('token', { signed: options.sign }) 61 | if (!token) return yield next 62 | var csrf = hasha(options.secret + '.' + token) 63 | this.state = assign(this.state || {}, { 64 | csrf: csrf 65 | }) 66 | yield next 67 | } 68 | 69 | /** 70 | * Validate a CSRF token 71 | * 72 | * @param {Object} options 73 | * @param {Generator} next 74 | */ 75 | 76 | function *verify (options, next) { 77 | // Get the token 78 | var token = this.cookies.get(options.cookie, { signed: options.sign }) 79 | if (!token) return this.throw(403, 'failed csrf check, no cookie value found') 80 | 81 | // get the CSRF token 82 | var csrf = get_csrf(this) 83 | if (!csrf) return this.throw(403, 'failed csrf check, no csrf token present') 84 | 85 | var hash = hasha(options.secret + '.' + token) 86 | 87 | // verify CSRF token passed in matches the hash 88 | if (hash == csrf) { 89 | yield next 90 | } else { 91 | this.throw(403, 'invalid csrf token') 92 | } 93 | } 94 | 95 | /** 96 | * Get the CSRF token 97 | * 98 | * @param {Application} ctx 99 | * @return {String|Boolean} 100 | */ 101 | 102 | function get_csrf(ctx) { 103 | var body = ctx.request.body 104 | return (body && body._csrf) 105 | || (ctx.query && ctx.query._csrf) 106 | || (ctx.get('x-csrf-token')) 107 | || (ctx.get('x-xsrf-token')) 108 | || ('string' == typeof body && body) 109 | } 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stateless-csrf", 3 | "version": "1.0.0", 4 | "description": "CSRF without sessions", 5 | "keywords": [ 6 | "csrf", 7 | "stateless", 8 | "authentication", 9 | "cookies" 10 | ], 11 | "author": "Matthew Mueller ", 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/koajs/stateless-csrf.git" 15 | }, 16 | "dependencies": { 17 | "hasha": "^1.0.1", 18 | "object-assign": "^3.0.0" 19 | }, 20 | "devDependencies": { 21 | "mocha": "^2.2.5", 22 | "roo": "^0.4.3", 23 | "should": "8.2.0", 24 | "supertest": "^1.0.1" 25 | }, 26 | "main": "index" 27 | } 28 | -------------------------------------------------------------------------------- /test/csrf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var request = require('supertest'); 6 | var server = require('./server'); 7 | var assert = require('assert'); 8 | 9 | describe('csrf', function() { 10 | 11 | it('should create a CSRF token on get requests', function(done) { 12 | request(server.listen()) 13 | .get('/') 14 | .set('Cookie', 'token=hiya') 15 | .end(function(err, res) { 16 | if (err) return done(err); 17 | assert.equal(res.text, 'ae16aaa64e64136e446d40a11a4bce1d57792861b01f0c3d574b4ed6303d22640f26650342733815628c854a0d8a05d639db176523a83fb93d8a90570d5da1f9') 18 | done(); 19 | }); 20 | }) 21 | 22 | it('should verify a CSRF token', function(done) { 23 | request(server.listen()) 24 | .post('/') 25 | .set('Cookie', 'token=hiya') 26 | .send({ _csrf: 'ae16aaa64e64136e446d40a11a4bce1d57792861b01f0c3d574b4ed6303d22640f26650342733815628c854a0d8a05d639db176523a83fb93d8a90570d5da1f9' }) 27 | .expect(200) 28 | .end(function(err, res) { 29 | if (err) return done(err); 30 | assert.equal(res.text, 'you are in!'); 31 | done(); 32 | }); 33 | }); 34 | 35 | it('should return 403 when there is no cookie present', function(done) { 36 | request(server.listen()) 37 | .post('/') 38 | .set('Accept', 'text/plain') 39 | .send({ _csrf: 'ae16aaa64e64136e446d40a11a4bce1d57792861b01f0c3d574b4ed6303d22640f26650342733815628c854a0d8a05d639db176523a83fb93d8a90570d5da1f9' }) 40 | .expect(403) 41 | .end(function(err, res) { 42 | assert.equal(res.text, 'failed csrf check, no cookie value found'); 43 | done(); 44 | }) 45 | }) 46 | 47 | it('should return 403 when there is no csrf token present', function(done) { 48 | request(server.listen()) 49 | .post('/') 50 | .set('Accept', 'text/plain') 51 | .set('Cookie', 'token=hiya') 52 | .expect(403) 53 | .end(function(err, res) { 54 | assert.equal(res.text, 'failed csrf check, no csrf token present'); 55 | done(); 56 | }) 57 | }) 58 | 59 | it('should return 403 when the csrf token doesnt match', function(done) { 60 | request(server.listen()) 61 | .post('/') 62 | .set('Accept', 'text/plain') 63 | .set('Cookie', 'token=hiya') 64 | .send({ _csrf: 'ae16aaa64e64136e446d40a11a4bce1d57792861b01f0c3d574b4ed6303d22640f26650342733815628c854a0d8a05d639db176523a83fb93d8a90570d5da1f9' }) 65 | .expect(200) 66 | .end(function(err, res) { 67 | assert.equal(res.text, 'you are in!'); 68 | done(); 69 | }) 70 | }) 71 | 72 | }) 73 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var roo = module.exports = require('roo')(); 6 | var csrf = require('../'); 7 | 8 | roo.use(csrf({ 9 | secret: 'some secret', 10 | cookie: 'token' 11 | })); 12 | 13 | // ignore emitted events 14 | roo.app.on('error', function(){}) 15 | 16 | roo.get('/', function * () { 17 | this.body = this.state.csrf; 18 | }) 19 | 20 | roo.post('/', function * () { 21 | this.body = 'you are in!'; 22 | }) 23 | --------------------------------------------------------------------------------