Comparing version 7.0.0 to 7.1.0
@@ -23,4 +23,6 @@ 'use strict'; | ||
cookieOptions: Joi.object().keys(null), | ||
headerName: Joi.string().optional(), | ||
restful: Joi.boolean().optional(), | ||
skip: Joi.func().optional() | ||
skip: Joi.func().optional(), | ||
logUnauthorized: Joi.boolean().optional() | ||
}); | ||
@@ -37,4 +39,6 @@ | ||
}, | ||
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 | ||
headerName: 'X-CSRF-Token', // Specify the name of the custom CSRF header | ||
restful: false, // Set to true for custom header crumb validation. Disables payload/query validation | ||
skip: false, // Set to a function which returns true when to skip crumb generation and validation, | ||
logUnauthorized: false // Set to true for crumb to write an event to the request log | ||
}; | ||
@@ -58,2 +62,9 @@ | ||
const unauthorizedLogger = () => { | ||
if (settings.logUnauthorized) { | ||
request.log(['crumb', 'unauthorized'], 'validation failed'); | ||
} | ||
}; | ||
// If skip function enabled. Call it and if returns true, do not attempt to do anything with crumb. | ||
@@ -103,3 +114,3 @@ | ||
if (!content || content instanceof Stream) { | ||
unauthorizedLogger(); | ||
throw Boom.forbidden(); | ||
@@ -109,2 +120,3 @@ } | ||
if (content[request.route.settings.plugins._crumb.key] !== request.plugins.crumb) { | ||
unauthorizedLogger(); | ||
throw Boom.forbidden(); | ||
@@ -124,5 +136,6 @@ } | ||
const header = request.headers['x-csrf-token']; | ||
const header = request.headers[settings.headerName.toLowerCase()]; | ||
if (!header) { | ||
unauthorizedLogger(); | ||
throw Boom.forbidden(); | ||
@@ -132,2 +145,3 @@ } | ||
if (header !== request.plugins.crumb) { | ||
unauthorizedLogger(); | ||
throw Boom.forbidden(); | ||
@@ -134,0 +148,0 @@ } |
{ | ||
"name": "crumb", | ||
"description": "CSRF crumb generation and validation plugin", | ||
"version": "7.0.0", | ||
"version": "7.1.0", | ||
"repository": "git://github.com/hapijs/crumb", | ||
@@ -6,0 +6,0 @@ "bugs": { |
@@ -71,5 +71,7 @@ ![crumb Logo](https://raw.github.com/hapijs/crumb/master/images/crumb.png) | ||
* `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. Default to `cookieOptions.path=/` | ||
* `headerName` - specify the name of the custom CSRF header. Defaults to `X-CSRF-Token`. | ||
* `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, h) {}`, which when provided, is called for every request. If the provided function returns true, validation and generation of crumb is skipped. Defaults to `false`. | ||
* `logUnauthorized` - whether to add to the request log with tag 'crumb' and data 'validation failed' (defaults to false) | ||
### Routes configuration | ||
@@ -76,0 +78,0 @@ |
@@ -23,3 +23,3 @@ 'use strict'; | ||
const str = '' + i; | ||
const buf = new Buffer(str, 'ascii'); | ||
const buf = Buffer.from(str, 'ascii'); | ||
this.push(buf); | ||
@@ -26,0 +26,0 @@ } |
@@ -315,2 +315,101 @@ 'use strict'; | ||
it('Adds to the request log if plugin option logUnauthorized is set to true', async () => { | ||
const server = new Hapi.Server(); | ||
let logFound; | ||
const preResponse = function (request, h) { | ||
const logs = request.logs; | ||
logFound = logs.find((log) => { | ||
return log.tags[0] === 'crumb' && log.data === 'validation failed'; | ||
}); | ||
return h.continue; | ||
}; | ||
server.ext('onPreResponse', preResponse); | ||
server.route({ | ||
method: 'POST', | ||
path: '/1', | ||
config: { | ||
log: { | ||
collect: true | ||
} | ||
}, | ||
handler: (request, h) => 'test' | ||
}); | ||
await server.register([ | ||
{ | ||
plugin: Crumb, | ||
options: { | ||
logUnauthorized: true | ||
} | ||
} | ||
]); | ||
const headers = {}; | ||
headers['X-API-Token'] = 'test'; | ||
await server.inject({ | ||
method: 'POST', | ||
url: '/1', | ||
headers | ||
}); | ||
expect(logFound).to.exist(); | ||
}); | ||
it('Does not add to the request log if plugin option logUnauthorized is set to false', async () => { | ||
const server = new Hapi.Server(); | ||
let logFound; | ||
const preResponse = function (request, h) { | ||
const logs = request.logs; | ||
logFound = logs.find((log) => { | ||
return log.tags[0] === 'crumb' && log.data === 'validation failed'; | ||
}); | ||
return h.continue; | ||
}; | ||
server.ext('onPreResponse', preResponse); | ||
server.route({ | ||
method: 'POST', | ||
path: '/1', | ||
config: { | ||
log: { | ||
collect: true | ||
} | ||
}, | ||
handler: (request, h) => 'test' | ||
}); | ||
await server.register([ | ||
{ | ||
plugin: Crumb, | ||
options: { | ||
logUnauthorized: false | ||
} | ||
} | ||
]); | ||
const headers = {}; | ||
headers['X-API-Token'] = 'test'; | ||
await server.inject({ | ||
method: 'POST', | ||
url: '/1', | ||
headers | ||
}); | ||
expect(logFound).to.not.exist(); | ||
}); | ||
it('should fail to register with bad options', async () => { | ||
@@ -832,2 +931,234 @@ | ||
}); | ||
it('validates crumb with a custom header name', async () => { | ||
const server = new Hapi.Server(); | ||
server.route([ | ||
{ | ||
method: 'GET', | ||
path: '/1', | ||
handler: (request, h) => { | ||
expect(request.plugins.crumb).to.exist(); | ||
expect(request.server.plugins.crumb.generate).to.exist(); | ||
return h.view('index', { | ||
title: 'test', | ||
message: 'hi' | ||
}); | ||
} | ||
}, | ||
{ | ||
method: 'POST', | ||
path: '/2', | ||
handler: (request, h) => { | ||
expect(request.payload).to.equal({ key: 'value' }); | ||
return 'valid'; | ||
} | ||
}, | ||
{ | ||
method: 'POST', | ||
path: '/3', | ||
options: { payload: { output: 'stream' } }, | ||
handler: (request, h) => 'never' | ||
}, | ||
{ | ||
method: 'PUT', | ||
path: '/4', | ||
handler: (request, h) => { | ||
expect(request.payload).to.equal({ key: 'value' }); | ||
return 'valid'; | ||
} | ||
}, | ||
{ | ||
method: 'PATCH', | ||
path: '/5', | ||
handler: (request, h) => { | ||
expect(request.payload).to.equal({ key: 'value' }); | ||
return 'valid'; | ||
} | ||
}, | ||
{ | ||
method: 'DELETE', | ||
path: '/6', | ||
handler: (request, h) => 'valid' | ||
}, | ||
{ | ||
method: 'POST', | ||
path: '/7', | ||
options: { | ||
plugins: { | ||
crumb: false | ||
} | ||
}, | ||
handler: (request, h) => { | ||
expect(request.payload).to.equal({ key: 'value' }); | ||
return 'valid'; | ||
} | ||
}, | ||
{ | ||
method: 'POST', | ||
path: '/8', | ||
options: { | ||
plugins: { | ||
crumb: { | ||
restful: false, | ||
source: 'payload' | ||
} | ||
} | ||
}, | ||
handler: (request, h) => { | ||
expect(request.payload).to.equal({ key: 'value' }); | ||
return 'valid'; | ||
} | ||
} | ||
]); | ||
await server.register([ | ||
Vision, | ||
{ | ||
plugin: Crumb, | ||
options: { | ||
restful: true, | ||
cookieOptions: { | ||
isSecure: true | ||
}, | ||
headerName: 'X-CUSTOM-TOKEN' | ||
} | ||
} | ||
]); | ||
server.views(internals.viewOptions); | ||
const res = await server.inject({ | ||
method: 'GET', | ||
url: '/1' | ||
}); | ||
const header = res.headers['set-cookie']; | ||
expect(header.length).to.equal(1); | ||
expect(header[0]).to.contain('Secure'); | ||
const cookie = header[0].match(/crumb=([^\x00-\x20\"\,\;\\\x7F]*)/); | ||
const validHeader = { | ||
cookie: 'crumb=' + cookie[1], | ||
'x-custom-token': cookie[1] | ||
}; | ||
const invalidHeader = { | ||
cookie: 'crumb=' + cookie[1], | ||
'x-custom-token': 'x' + cookie[1] | ||
}; | ||
expect(res.result).to.equal(Views.viewWithCrumb(cookie[1])); | ||
const res2 = await server.inject({ | ||
method: 'POST', | ||
url: '/2', | ||
payload: '{ "key": "value" }', | ||
headers: validHeader | ||
}); | ||
expect(res2.result).to.equal('valid'); | ||
const res3 = await server.inject({ | ||
method: 'POST', | ||
url: '/2', | ||
payload: '{ "key": "value" }', | ||
headers: invalidHeader | ||
}); | ||
expect(res3.statusCode).to.equal(403); | ||
const res4 = await server.inject({ | ||
method: 'POST', | ||
url: '/3', | ||
headers: { | ||
cookie: 'crumb=' + cookie[1] | ||
} | ||
}); | ||
expect(res4.statusCode).to.equal(403); | ||
const res5 = await server.inject({ | ||
method: 'PUT', | ||
url: '/4', | ||
payload: '{ "key": "value" }', | ||
headers: validHeader | ||
}); | ||
expect(res5.result).to.equal('valid'); | ||
const res6 = await server.inject({ | ||
method: 'PUT', | ||
url: '/4', | ||
payload: '{ "key": "value" }', | ||
headers: invalidHeader | ||
}); | ||
expect(res6.statusCode).to.equal(403); | ||
const res7 = await server.inject({ | ||
method: 'PATCH', | ||
url: '/5', | ||
payload: '{ "key": "value" }', | ||
headers: validHeader | ||
}); | ||
expect(res7.result).to.equal('valid'); | ||
const res8 = await server.inject({ | ||
method: 'PATCH', | ||
url: '/5', | ||
payload: '{ "key": "value" }', | ||
headers: invalidHeader | ||
}); | ||
expect(res8.statusCode).to.equal(403); | ||
const res9 = await server.inject({ | ||
method: 'DELETE', | ||
url: '/6', | ||
headers: validHeader | ||
}); | ||
expect(res9.result).to.equal('valid'); | ||
const res10 = await server.inject({ | ||
method: 'DELETE', | ||
url: '/6', | ||
headers: invalidHeader | ||
}); | ||
expect(res10.statusCode).to.equal(403); | ||
const res11 = await server.inject({ | ||
method: 'POST', | ||
url: '/7', | ||
payload: '{ "key": "value" }' | ||
}); | ||
expect(res11.result).to.equal('valid'); | ||
const payload = { key: 'value', crumb: cookie[1] }; | ||
delete validHeader['x-custom-token']; | ||
const res12 = await server.inject({ | ||
method: 'POST', | ||
url: '/8', | ||
payload: JSON.stringify(payload), | ||
headers: validHeader | ||
}); | ||
expect(res12.statusCode).to.equal(200); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
115234
1194
83
0