All files / server/security api-request-guard.ts

100% Statements 14/14
100% Branches 12/12
100% Functions 2/2
100% Lines 14/14

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36    1x   1x                     10x 1x 1x   9x 9x 2x 2x     7x 3x 3x   4x       7x    
import type { NextFunction, Request, Response } from "express";
 
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
/** Fetch metadata values a page of this origin or the user itself produces. */
const ALLOWED_FETCH_SITES = new Set(["same-origin", "none"]);
 
/**
 * Cross-site request guard for the JSON API. Browsers send a form or text/plain
 * POST cross-site without a CORS preflight, so a foreign page could otherwise drive
 * the API with the victim's network position or identity (in IP gateway mode the
 * network is the access boundary). Mutating requests with a body must be JSON,
 * which a foreign page cannot send without a preflight this server never grants,
 * and browsers that report a cross-site or same-site sender are refused outright.
 */
export function guardApiMutations(req: Request, res: Response, next: NextFunction): void {
  if (!MUTATING_METHODS.has(req.method)) {
    next();
    return;
  }
  const fetchSite = req.get("sec-fetch-site");
  if (fetchSite !== undefined && !ALLOWED_FETCH_SITES.has(fetchSite)) {
    res.status(403).json({ error: "Cross-site API requests are not allowed" });
    return;
  }
  // `req.is` is null without a body and false for any other media type; an empty body carries no data.
  if (!hasEmptyBody(req) && req.is("application/json") === false) {
    res.status(415).json({ error: "API requests must be sent as application/json" });
    return;
  }
  next();
}
 
function hasEmptyBody(req: Request): boolean {
  return req.headers["transfer-encoding"] === undefined && req.headers["content-length"] === "0";
}