Skip to content
This repository has been archived by the owner on Sep 14, 2022. It is now read-only.

Support for CSRF token patterns as instructed by OWASP. #263

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ unreleased
- Fix `maxAge` option to reject invalid values
* deps: [email protected]
- deps: [email protected]
* Allow selection of `csrfTokenPattern`. Support for HMAC based token pattern and encryption based token pattern added. Defaults to double submit cookie pattern.

1.11.0 / 2020-01-18
===================
Expand Down
40 changes: 34 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

Node.js [CSRF][wikipedia-csrf] protection middleware.

Requires either a session middleware or [cookie-parser](https://www.npmjs.com/package/cookie-parser) to be initialized first.
If not using stateless CSRF token patterns ([HMAC based token pattern][owasp-csrf-hmac-based-token] or
[Encryption based token pattern][owasp-csrf-encryption-based-token]), it requires either a session middleware
or [cookie-parser](https://www.npmjs.com/package/cookie-parser) to be initialized first.

* If you are setting the ["cookie" option](#cookie) to a non-`false` value,
then you must use [cookie-parser](https://www.npmjs.com/package/cookie-parser)
Expand Down Expand Up @@ -40,19 +42,31 @@ var csurf = require('csurf')
Create a middleware for CSRF token creation and validation. This middleware
adds a `req.csrfToken()` function to make a token which should be added to
requests which mutate state, within a hidden form field, query-string etc.
This token is validated against the visitor's session or csrf cookie.

#### Options

The `csurf` function takes an optional `options` object that may contain
any of the following keys:

##### csrfTokenPattern

Determines the CSRF token pattern to use.
Pass `hmac` for [HMAC based token pattern][owasp-csrf-hmac-based-token].
Pass `encrypted` for [Encryption based token pattern][owasp-csrf-encryption-based-token]
Defaults to [double submit cookie pattern][owsap-csrf-double-submit].

NOTE: In case of HMAC based token pattern,
`req._csrfUserId`, `req._csrfNonce` and `req._csrfOperation` are used to generate the token. Also, `req._csrfUserId`
is used for verifying the token value.

NOTE: In case of Encryption based token pattern,
`req._csrfUserId` and `req._csrfNonce` are used to generate the token. Also, `req._csrfUserId`
is used for verifying the token value.

##### cookie

Determines if the token secret for the user should be stored in a cookie
or in `req.session`. Storing the token secret in a cookie implements
the [double submit cookie pattern][owsap-csrf-double-submit].
Defaults to `false`.
or in `req.session`. Defaults to `false`. This option makes no difference in case `csrfTokenPattern` is `hmac` or `encrypted`.

When set to `true` (or an object of options for the cookie), then the module
changes behavior and no longer uses `req.session`. This means you _are no
Expand Down Expand Up @@ -93,7 +107,7 @@ Defaults to `'session'` (i.e. looks at `req.session`). The CSRF secret
from this library is stored and read as `req[sessionKey].csrfSecret`.

If the ["cookie" option](#cookie) is not `false`, then this option does
nothing.
nothing. This option makes no difference in case `csrfTokenPattern` is `hmac` or `encrypted`.

##### value

Expand All @@ -112,6 +126,18 @@ locations, in order:
- `req.headers['x-csrf-token']` - the `X-CSRF-Token` HTTP request header.
- `req.headers['x-xsrf-token']` - the `X-XSRF-Token` HTTP request header.

##### hmacSecret

Applicable only if `csrfTokenPattern` is `hmac`. This is secret known only to the server and is used in the HMAC algorithm.

##### encryptionKey

Applicable only if `csrfTokenPattern` is `encrypted`. This is secret key known only to the server and is used in the Encryption algorithm (aes-256-cbc).

##### expiry

Applicable only if `csrfTokenPattern` is `hmac` or `encrypted`. This is the token expiry in seconds. Expired tokens are rejected.

## Example

### Simple express example
Expand Down Expand Up @@ -306,6 +332,8 @@ app.use(function (err, req, res, next) {

[owsap-csrf]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
[owsap-csrf-double-submit]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
[owasp-csrf-hmac-based-token]: https://owasp.deteact.com/cheat/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#hmac-based-token-pattern
[owasp-csrf-encryption-based-token]: https://owasp.deteact.com/cheat/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#encryption-based-token-pattern
[wikipedia-csrf]: https://en.wikipedia.org/wiki/Cross-site_request_forgery

## License
Expand Down
37 changes: 21 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
var Cookie = require('cookie')
var createError = require('http-errors')
var sign = require('cookie-signature').sign
var Tokens = require('csrf')
var TokenManagerFactory = require('./tokenManager/Factory')

/**
* Module exports.
Expand Down Expand Up @@ -50,8 +50,10 @@ function csurf (options) {
// get value getter
var value = opts.value || defaultValue

var isStatelessPattern = opts.csrfTokenPattern === 'hmac' || opts.csrfTokenPattern === 'encrypted'

// token repo
var tokens = new Tokens(opts)
var tokenManager = new TokenManagerFactory(opts).getTokenManager()

// ignored methods
var ignoreMethods = opts.ignoreMethods === undefined
Expand All @@ -66,18 +68,21 @@ function csurf (options) {
var ignoreMethod = getIgnoredMethods(ignoreMethods)

return function csrf (req, res, next) {
// validate the configuration against request
if (!verifyConfiguration(req, sessionKey, cookie)) {
return next(new Error('misconfigured csrf'))
}
var token, secret

// get the secret from the request
var secret = getSecret(req, sessionKey, cookie)
var token
if (!isStatelessPattern) {
// validate the configuration against request
if (!verifyConfiguration(req, sessionKey, cookie)) {
return next(new Error('misconfigured csrf'))
}

// get the secret from the request
secret = getSecret(req, sessionKey, cookie)
}

// lazy-load token getter
req.csrfToken = function csrfToken () {
var sec = !cookie
var sec = !cookie && !isStatelessPattern
? getSecret(req, sessionKey, cookie)
: secret

Expand All @@ -87,28 +92,28 @@ function csurf (options) {
}

// generate & set new secret
if (sec === undefined) {
sec = tokens.secretSync()
if (sec === undefined && !isStatelessPattern) {
sec = tokenManager.secretSync()
setSecret(req, res, sessionKey, sec, cookie)
}

// update changed secret
secret = sec

// create new token
token = tokens.create(secret)
token = tokenManager.create(secret, req)

return token
}

// generate & set secret
if (!secret) {
secret = tokens.secretSync()
if (!secret && !isStatelessPattern) {
secret = tokenManager.secretSync()
setSecret(req, res, sessionKey, secret, cookie)
}

// verify the incoming token
if (!ignoreMethod[req.method] && !tokens.verify(secret, value(req))) {
if (!ignoreMethod[req.method] && !tokenManager.verify(secret, value(req), req)) {
return next(createError(403, 'invalid csrf token', {
code: 'EBADCSRFTOKEN'
}))
Expand Down
211 changes: 211 additions & 0 deletions test/testEncrypted.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@

process.env.NODE_ENV = 'test'

var connect = require('connect')
var http = require('http')
var bodyParser = require('body-parser')
var querystring = require('querystring')
var request = require('supertest')

var csurf = require('..')

describe('csurf with Encryption based token pattern', function () {
it('should work in req.body', function (done) {
var server = createServerWithoutCookieAndSession({
csrfTokenPattern: 'encrypted',
encryptionKey: '5e91b89e38697ee924ba9b9940217cf654ecdeed4dc0f551a88c3b7f577469f0'
})

request(server)
.get('/')
.expect(200, function (err, res) {
if (err) return done(err)
var token = res.text

request(server)
.post('/')
.send('_csrf=' + encodeURIComponent(token))
.expect(200, done)
})
})
})

it('should work in req.query', function (done) {
var server = createServerWithoutCookieAndSession({
csrfTokenPattern: 'encrypted',
encryptionKey: '5e91b89e38697ee924ba9b9940217cf654ecdeed4dc0f551a88c3b7f577469f0'
})

request(server)
.get('/')
.expect(200, function (err, res) {
if (err) return done(err)
var token = res.text

request(server)
.post('/?_csrf=' + encodeURIComponent(token))
.expect(200, done)
})
})

it('should work in csrf-token header', function (done) {
var server = createServerWithoutCookieAndSession({
csrfTokenPattern: 'encrypted',
encryptionKey: '5e91b89e38697ee924ba9b9940217cf654ecdeed4dc0f551a88c3b7f577469f0'
})

request(server)
.get('/')
.expect(200, function (err, res) {
if (err) return done(err)
var token = res.text

request(server)
.post('/')
.set('csrf-token', token)
.expect(200, done)
})
})

it('should work in xsrf-token header', function (done) {
var server = createServerWithoutCookieAndSession({
csrfTokenPattern: 'encrypted',
encryptionKey: '5e91b89e38697ee924ba9b9940217cf654ecdeed4dc0f551a88c3b7f577469f0'
})

request(server)
.get('/')
.expect(200, function (err, res) {
if (err) return done(err)
var token = res.text

request(server)
.post('/')
.set('xsrf-token', token)
.expect(200, done)
})
})

it('should work in x-csrf-token header', function (done) {
var server = createServerWithoutCookieAndSession({
csrfTokenPattern: 'encrypted',
encryptionKey: '5e91b89e38697ee924ba9b9940217cf654ecdeed4dc0f551a88c3b7f577469f0'
})

request(server)
.get('/')
.expect(200, function (err, res) {
if (err) return done(err)
var token = res.text

request(server)
.post('/')
.set('x-csrf-token', token)
.expect(200, done)
})
})

it('should work in x-xsrf-token header', function (done) {
var server = createServerWithoutCookieAndSession({
csrfTokenPattern: 'encrypted',
encryptionKey: '5e91b89e38697ee924ba9b9940217cf654ecdeed4dc0f551a88c3b7f577469f0'
})

request(server)
.get('/')
.expect(200, function (err, res) {
if (err) return done(err)
var token = res.text

request(server)
.post('/')
.set('x-xsrf-token', token)
.expect(200, done)
})
})

it('should fail with an invalid token', function (done) {
var server = createServerWithoutCookieAndSession({
csrfTokenPattern: 'encrypted',
encryptionKey: '5e91b89e38697ee924ba9b9940217cf654ecdeed4dc0f551a88c3b7f577469f0'
})

request(server)
.get('/')
.expect(200, function (err, res) {
if (err) return done(err)
request(server)
.post('/')
.set('X-CSRF-Token', '42')
.expect(403, done)
})
})

it('should fail with no token', function (done) {
var server = createServerWithoutCookieAndSession({
csrfTokenPattern: 'encrypted',
encryptionKey: '5e91b89e38697ee924ba9b9940217cf654ecdeed4dc0f551a88c3b7f577469f0'
})

request(server)
.get('/')
.expect(200, function (err, res) {
if (err) return done(err)
request(server)
.post('/')
.expect(403, done)
})
})

it('should fail with expired token', function (done) {
var server = createServerWithoutCookieAndSession({
csrfTokenPattern: 'encrypted',
encryptionKey: '5e91b89e38697ee924ba9b9940217cf654ecdeed4dc0f551a88c3b7f577469f0',
expiry: 0.5
})

// Token expiry is 0.5 seconds and we use it after 1 second.

request(server)
.get('/')
.expect(200, function (err, res) {
if (err) return done(err)
var token = res.text

setTimeout(function () {
request(server)
.post('/')
.set('x-xsrf-token', token)
.expect(403, done)
}, 1000)
})
})

function createServerWithoutCookieAndSession (opts) {
var app = connect()

app.use(function (req, res, next) {
var index = req.url.indexOf('?') + 1

if (index) {
req.query = querystring.parse(req.url.substring(index))
}

next()
})
app.use(bodyParser.urlencoded({ extended: false }))

app.use(function(req, res, next) {
req._csrfUserId = 123
req._csrfNonce = 1
next()
})

app.use(csurf(opts))

app.use(function (req, res) {
res.end(req.csrfToken() || 'none')
})

return http.createServer(app)
}
Loading