79 lines
2.4 KiB
JavaScript
79 lines
2.4 KiB
JavaScript
const { Headers } = require("../constants")
|
|
const { buildMatcherRegex, matches } = require("./matchers")
|
|
|
|
/**
|
|
* GET, HEAD and OPTIONS methods are considered safe operations
|
|
*
|
|
* POST, PUT, PATCH, and DELETE methods, being state changing verbs,
|
|
* should have a CSRF token attached to the request
|
|
*/
|
|
const EXCLUDED_METHODS = ["GET", "HEAD", "OPTIONS"]
|
|
|
|
/**
|
|
* There are only three content type values that can be used in cross domain requests.
|
|
* If any other value is used, e.g. application/json, the browser will first make a OPTIONS
|
|
* request which will be protected by CORS.
|
|
*/
|
|
const INCLUDED_CONTENT_TYPES = [
|
|
"application/x-www-form-urlencoded",
|
|
"multipart/form-data",
|
|
"text/plain",
|
|
]
|
|
|
|
/**
|
|
* Validate the CSRF token generated aganst the user session.
|
|
* Compare the token with the x-csrf-token header.
|
|
*
|
|
* If the token is not found within the request or the value provided
|
|
* does not match the value within the user session, the request is rejected.
|
|
*
|
|
* CSRF protection provided using the 'Synchronizer Token Pattern'
|
|
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
|
|
*
|
|
*/
|
|
module.exports = (opts = { noCsrfPatterns: [] }) => {
|
|
const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns)
|
|
return async (ctx, next) => {
|
|
// don't apply for excluded paths
|
|
const found = matches(ctx, noCsrfOptions)
|
|
if (found) {
|
|
return next()
|
|
}
|
|
|
|
// don't apply for the excluded http methods
|
|
if (EXCLUDED_METHODS.indexOf(ctx.method) !== -1) {
|
|
return next()
|
|
}
|
|
|
|
// don't apply when the content type isn't supported
|
|
let contentType = ctx.get("content-type")
|
|
? ctx.get("content-type").toLowerCase()
|
|
: ""
|
|
if (
|
|
!INCLUDED_CONTENT_TYPES.filter(type => contentType.includes(type)).length
|
|
) {
|
|
return next()
|
|
}
|
|
|
|
// don't apply csrf when the internal api key has been used
|
|
if (ctx.internal) {
|
|
return next()
|
|
}
|
|
|
|
// apply csrf when there is a token in the session (new logins)
|
|
// in future there should be a hard requirement that the token is present
|
|
const userToken = ctx.user.csrfToken
|
|
if (!userToken) {
|
|
return next()
|
|
}
|
|
|
|
// reject if no token in request or mismatch
|
|
const requestToken = ctx.get(Headers.CSRF_TOKEN)
|
|
if (!requestToken || requestToken !== userToken) {
|
|
ctx.throw(403, "Invalid CSRF token")
|
|
}
|
|
|
|
return next()
|
|
}
|
|
}
|