UNPKG

@dr.pogodin/csurf

Version:

CSRF token middleware for ExpressJS

265 lines (240 loc) 8.12 kB
"use strict"; 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