api/csrf.js

/*
 *  Author: Vlad Seryakov vseryakov@gmail.com
 *  backendjs 2018
 */

/**
  * @module api/csrf
  */

const lib = require(__dirname + '/../lib');
const logger = require(__dirname + '/../logger');

const mod =

/**
 * ## Origin/Sec-Fetch-Site headers checks
 *
 * `api-csrf-origin` and/or `api-csrf-sec-fetch` config parameters must be configured,
 * only matched paths or locations are checked, so CSRF protection is explicit by the config.
 *
 * @example <caption>Only allow specific origins for /account</caption>
 * api-csrf-origin-^/account = http://app.host.com
 * api-csrf-origin-^/account = https://host.com,http://localhost
 *
 * @example <caption>Only allow same-site or same-origin Sec-Fetch-Site for /api</caption>
 * api-csrf-sec-fetch-^/api/ = same-site
 * api-csrf-sec-fetch-^/api/ = same-origin,same-origin
 *
 * @example <caption>Only allow same-origin Sec-Fetch-Site</caption>
 * api-csrf-sec-fetch-^/ = same-origin
 *
 */

module.exports = {
    name: "api.csrf",
    args: [
        { name: "err-(.+)", descr: "Error messages for various cases" },
        { name: "origin-(.+)", obj: "origin", make: "$1", type: "regexpobj", reverse: 1, nocamel: 1, onupdate: () => { for (const p in this.origin) delete this.origin[p].values }, descr: "Regexp for URLs to by allowed by origin", example: "api-csrf-origin-^/account = http://host.com\napi-csrf-origin-^/account = https://host.com,http://localhost" },
        { name: "sec-fetch-(.+)", obj: "secFetch", make: "$1", type: "regexpobj", reverse: 1, nocamel: 1, onupdate: () => { for (const p in this.secFetch) delete this.secFetch[p].values }, descr: "Regexp for URLs to use specific Sec-Fetch-Site header validation by: same-origin, same-site, cross-site, none", example: "api-csrf-sec-fetch-^/webhook = cross-site\napi-csrf-sec-fetch-^/ = same-origin,same-site" },
        { name: "skip-method", type: "list", descr: "Do not check for specified methods" },
    ],
    skipMethod: ["GET", "HEAD", "OPTIONS", "TRACE" ],

    errInvalidCsrf: "Authentication failed",
};

/**
 * Verify Origin and Sec-Fetch-Site headers for non-skipping methods
 * @param {IncomingRequest} req - Express request
 * @returns {undefinded|object} - an error object if not valid
 * @memberof module:api/csrf
 * @method check
 */
mod.check = function(req)
{
    if (mod.skipMethod.includes(req.method)) return;
    var err = mod.checkOrigin(req);
    if (!err) err = mod.checkFetchSite(req)
    return err;
}

/**
 * Verify Origin header
 * @param {IncomingRequest} req - Express request
 * @returns {undefinded|object} - an error object if not valid
 * @memberof module:api/csrf
 * @method checkOrigin
 */

mod.checkOrigin = function(req)
{
    const location = req.options.hostname + req.options.path;

    const origin = req.header('origin');
    for (const p in mod.origin) {
        const rule = mod.origin[p];
        if (lib.testRegexpObj(location, rule) || lib.testRegexpObj(req.options.path, rule)) {
            if (!rule.values) rule.values = lib.strSplit(p);
            if (!origin || !rule.values.includes(origin)) {
                logger.debug("checkOrigin:", mod.name, req.options, "HDR:", req.headers, "CONF:", p, rule);
                return { status: 403, message: mod.errInvalidCsrf, code: "NOORIGIN" };
            }
        }
    }
}

/**
 * Verify Sec-Fetch-Site headers
 * @param {IncomingRequest} req - Express request
 * @returns {undefinded|object} - an error object if not valid
 * @memberof module:api/csrf
 * @method checkFetchSite
 */
mod.checkFetchSite = function(req)
{
    const location = req.options.hostname + req.options.path;

    const secFetchSite = req.header('sec-fetch-site');
    for (const p in mod.secFetch) {
        const rule = mod.secFetch[p];
        if (lib.testRegexpObj(location, rule) || lib.testRegexpObj(req.options.path, rule)) {
            if (!rule.values) rule.values = lib.strSplit(p);
            if (!secFetchSite || !rule.values.includes(secFetchSite)) {
                logger.debug("checkSecFetchSite:", mod.name, req.options, "HDR:", req.headers, "CONF:", p, rule);
                return { status: 403, message: mod.errInvalidCsrf, code: "NOSECFETCHSITE" };
            }
        }
    }
}