@dr.pogodin/csurf
Version:
CSRF token middleware for ExpressJS
265 lines (240 loc) • 8.12 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _cookie = require("cookie");
var _httpErrors = _interopRequireDefault(require("http-errors"));
var _cookieSignature = require("cookie-signature");
var _tokens = _interopRequireWildcard(require("./tokens"));
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
// TODO: This should come from a cookie library.
/**
* Get options for cookie.
*
* @param {boolean|object} [options]
*/
function getCookieOptions(options) {
if (options !== true && typeof options !== 'object') {
return undefined;
}
const opts = {
key: '_csrf',
path: '/'
};
if (typeof options === 'object') {
for (const [key, value] of Object.entries(options)) {
// TODO: It actually breaks one of existing tests, if we don't check
// for it. Perhaps, we should correct typings, or do some other refactoring
// to avoid this.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (value !== undefined) {
opts[key] = value;
}
}
}
return opts;
}
/**
* Default value function, checking the `req.body`
* and `req.query` for the CSRF token.
*
* @param req
* @return
*/
function defaultValue(req) {
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
// eslint-disable-next-line no-underscore-dangle
return req.body?._csrf || req.query?._csrf || req.headers['csrf-token'] || req.headers['xsrf-token'] || req.headers['x-csrf-token'] || req.headers['x-xsrf-token'];
/* eslint-enable @typescript-eslint/prefer-nullish-coalescing */
}
// TODO: Actually, we should type `methods` stricter, limiting it to the valid
// method name literals.
/**
* Get a lookup of ignored methods.
*
* @param {array} methods
* @returns {object}
* @api private
*/
function getIgnoredMethods(methods) {
const obj = {};
for (const method of methods) {
obj[method.toUpperCase()] = true;
}
return obj;
}
/**
* Get the token secret bag from the request.
*
* @param {IncomingMessage} req
* @param {String} sessionKey
* @param {Object} [cookie]
* @api private
*/
function getSecretBag(req, sessionKey, cookie) {
if (cookie) {
// get secret from cookie
const cookieKey = cookie.signed ? 'signedCookies' : 'cookies';
return req[cookieKey];
}
// TODO: A less forceful type casting would be nice to have here.
// get secret from session
return req[sessionKey];
}
/**
* Get the token secret from the request.
*
* @param {IncomingMessage} req
* @param {String} sessionKey
* @param {Object} [cookie]
* @api private
*/
function getSecret(req, sessionKey, cookie) {
// get the bag & key
const bag = getSecretBag(req, sessionKey, cookie);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const key = cookie?.key || 'csrfSecret';
if (!bag) {
throw new Error('misconfigured csrf');
}
// return secret from bag
return bag[key];
}
/**
* Set a cookie on the HTTP response.
*
* @param {OutgoingMessage} res
* @param {string} name
* @param {string} val
* @param {Object} [options]
* @api private
*/
function setCookie(res, name, val, options) {
const data = (0, _cookie.serialize)(name, val, options);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const rawPrev = res.getHeader('set-cookie') || [];
const prev = typeof rawPrev === 'number' ? rawPrev.toString() : rawPrev;
const header = Array.isArray(prev) ? prev.concat(data) : [prev, data];
res.setHeader('set-cookie', header);
}
/**
* Set the token secret on the request.
*
* @param {IncomingMessage} req
* @param {OutgoingMessage} res
* @param {string} sessionKey
* @param {string} val
* @param {Object} [cookie]
* @api private
*/
function setSecret(req, res, sessionKey, val, cookie) {
if (cookie) {
// set secret on cookie
let value = val;
if (cookie.signed) {
// NOTE: This one is not expected to be hit, as if the cookie signature
// secret is not properly configured via "cookie-parser" middleware, that
// is checked and throws earlier in the code.
if (!req.secret) throw Error('Internal error');
value = `s:${(0, _cookieSignature.sign)(val, req.secret)}`;
}
setCookie(res, cookie.key, value, cookie);
} else {
// set secret on session
// TODO: Can we type it in a better way, to avoid such forced type-cast?
// eslint-disable-next-line no-param-reassign
req[sessionKey].csrfSecret = val;
}
}
/**
* Verify the configuration against the request.
* @private
*/
function verifyConfiguration(req, sessionKey, cookie) {
if (!getSecretBag(req, sessionKey, cookie)) {
return false;
}
// NOTE: `req.secret` is the cookie signature secret, configured and set by
// "cookie-parser" middleware: https://github.com/expressjs/cookie-parser
if (cookie && cookie.signed && !req.secret) {
return false;
}
return true;
}
/**
* CSRF protection middleware.
*
* 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.
*
* @param {Object} options
* @return {Function} middleware
* @public
*/
function csurf(options = {}) {
// get cookie options
const cookie = getCookieOptions(options.cookie);
// get session options
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const sessionKey = options.sessionKey || 'session';
// get value getter
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const value = options.value || defaultValue;
// token repo
const tokens = new _tokens.default(options);
// ignored methods
const ignoreMethods = options.ignoreMethods ?? ['GET', 'HEAD', 'OPTIONS'];
if (!Array.isArray(ignoreMethods)) {
throw new TypeError('option ignoreMethods must be an array');
}
// generate lookup
const ignoreMethod = getIgnoredMethods(ignoreMethods);
return (req, res, next) => {
// validate the configuration against request
if (!verifyConfiguration(req, sessionKey, cookie)) {
next(new Error('misconfigured csrf'));
return;
}
// get the secret from the request
let secret = getSecret(req, sessionKey, cookie);
let token;
// lazy-load token getter
// eslint-disable-next-line no-param-reassign
req.csrfToken = () => {
let sec = cookie ? secret : getSecret(req, sessionKey, cookie);
// use cached token if secret has not changed
if (token && sec === secret) {
return token;
}
// generate & set new secret
if (sec === undefined) {
sec = tokens.secretSync();
setSecret(req, res, sessionKey, sec, cookie);
}
// update changed secret
secret = sec;
// create new token
token = tokens.create(secret);
return token;
};
// generate & set secret
if (!secret) {
secret = tokens.secretSync();
setSecret(req, res, sessionKey, secret, cookie);
}
// verify the incoming token
if (!ignoreMethod[req.method] && !(0, _tokens.verify)(secret, value(req))) {
next((0, _httpErrors.default)(403, 'invalid csrf token', {
code: 'EBADCSRFTOKEN'
}));
return;
}
next();
};
}
var _default = exports.default = csurf;
//# sourceMappingURL=index.js.map