UNPKG

@adonisjs/shield

Version:

A middleware for AdonisJS to keep web applications secure from common web attacks

275 lines (264 loc) 7.95 kB
import { E_BAD_CSRF_TOKEN, cspKeywords } from "./chunk-IGRWBR6H.js"; // src/guards/hsts.ts import string from "@adonisjs/core/helpers/string"; // src/noop.ts function noop(_) { } // src/guards/hsts.ts var DEFAULT_MAX_AGE = 180 * 24 * 60 * 60; function normalizeMaxAge(maxAge) { if (maxAge === null || maxAge === void 0) { return DEFAULT_MAX_AGE; } const maxAgeInSeconds = string.seconds.parse(maxAge); if (maxAgeInSeconds < 0) { throw new Error('Max age for "shield.hsts" cannot be a negative value'); } return maxAgeInSeconds; } function hstsFactory(options) { if (!options.enabled) { return noop; } const maxAge = normalizeMaxAge(options.maxAge); let value = `max-age=${maxAge}`; if (options.includeSubDomains) { value += "; includeSubDomains"; } if (options.preload) { value += "; preload"; } return function hsts({ response }) { response.header("Strict-Transport-Security", value); }; } // src/guards/csrf.ts import Tokens from "csrf"; // src/debug.ts import { debuglog } from "node:util"; var debug_default = debuglog("adonisjs:shield"); // src/guards/csrf.ts var CsrfGuard = class { /** * Factory for generate csrf secrets and tokens */ #tokens = new Tokens(); /** * An array of methods on which the CSRF validation should be enforced. */ #allowedMethods; /** * An array of routes to be ignored from CSRF validation */ #routesToIgnore; /** * Name of the csrf secret key stored inside the session store. * The secret key is used to validate the tokens */ #secretSessionKey = "csrf-secret"; /** * Csrf options */ #options; /** * Reference to the encryption module */ #encryption; /** * Reference to the view provider */ #edge; constructor(options, encryption, edge) { this.#options = options; this.#encryption = encryption; this.#edge = edge; this.#routesToIgnore = this.#options.exceptRoutes || []; this.#allowedMethods = (this.#options.methods || []).map((method) => method.toLowerCase()); } /** * Find if a request should be validated or not */ #shouldValidateRequest(ctx) { if (this.#allowedMethods.length && !this.#allowedMethods.includes(ctx.request.method().toLowerCase())) { debug_default('csrf: ignoring request for "%s" method', ctx.request.method()); return false; } if (typeof this.#routesToIgnore === "function") { return !this.#routesToIgnore(ctx); } if (this.#routesToIgnore.includes(ctx.route.pattern)) { debug_default('csrf: ignoring route "%s"', ctx.route.pattern); return false; } return true; } /** * Read csrf token from one of the following sources. * * - `_csrf` input * - `x-csrf-token` header * - Or `x-xsrf-token` header. The header value must be set by * reading the `XSRF-TOKEN` cookie. */ #getCsrfTokenFromRequest({ request }) { if (request.input("_csrf")) { debug_default('retrieved token from "_csrf" input'); return request.input("_csrf"); } if (request.header("x-csrf-token")) { debug_default('retrieved token from "x-csrf-token" header'); return request.header("x-csrf-token"); } if (!this.#options.enableXsrfCookie) { return null; } const encryptedToken = request.header("x-xsrf-token"); if (typeof encryptedToken !== "string" || !encryptedToken) { return null; } debug_default('retrieved token from "x-xsrf-token" header'); return this.#encryption.decrypt(decodeURIComponent(encryptedToken).slice(2), "XSRF-TOKEN"); } /** * Share csrf helper methods with the view engine. */ #shareCsrfViewLocals(ctx) { if (!ctx.view || !this.#edge) { return; } ctx.view.share({ csrfToken: ctx.request.csrfToken, csrfMeta: () => { return this.#edge.globals.html.safe( `<meta name='csrf-token' content='${ctx.request.csrfToken}'>` ); }, csrfField: () => { return this.#edge.globals.html.safe( `<input type='hidden' name='_csrf' value='${ctx.request.csrfToken}'>` ); } }); } /** * Generate a new csrf token using the csrf secret extracted from session. */ #generateCsrfToken(csrfSecret) { return this.#tokens.create(csrfSecret); } /** * Return the existing CSRF secret from the session or create a * new one. Newly created secret is persisted to session at * the same time */ async #getCsrfSecret(ctx) { let csrfSecret = ctx.session.get(this.#secretSessionKey); if (!csrfSecret) { debug_default("generating new CSRF secret"); csrfSecret = await this.#tokens.secret(); ctx.session.put(this.#secretSessionKey, csrfSecret); } return csrfSecret; } /** * Handle csrf verification. First, get the secret, * next, check if the request method should be * verified. Next, attach the newly generated * csrf token to the request object. */ async handle(ctx) { const csrfSecret = await this.#getCsrfSecret(ctx); ctx.request.csrfToken = this.#generateCsrfToken(csrfSecret); if (this.#options.enableXsrfCookie) { ctx.response.encryptedCookie("XSRF-TOKEN", ctx.request.csrfToken, { ...this.#options.cookieOptions, httpOnly: false }); } this.#shareCsrfViewLocals(ctx); if (this.#shouldValidateRequest(ctx)) { const csrfToken = this.#getCsrfTokenFromRequest(ctx); if (!csrfToken || !this.#tokens.verify(csrfSecret, csrfToken)) { throw new E_BAD_CSRF_TOKEN(); } } } }; function csrfFactory(options, encryption, edge) { if (!options.enabled) { return noop; } const csrfGuard = new CsrfGuard(options, encryption, edge); return csrfGuard.handle.bind(csrfGuard); } // src/guards/csp/main.ts import helmetCsp from "helmet-csp"; import string2 from "@adonisjs/core/helpers/string"; cspKeywords.register("@nonce", function(_, response) { return `'nonce-${response.nonce}'`; }); function cspFactory(options) { if (!options.enabled) { return noop; } if (options.directives) { Object.keys(options.directives).forEach((directive) => { options.directives[directive] = cspKeywords.resolve(options.directives[directive]); }); } const helmetCspMiddleware = helmetCsp(options); return function csp(ctx) { return new Promise((resolve, reject) => { ctx.response.nonce = string2.generateRandom(16); ctx.response.response.nonce = ctx.response.nonce; if ("view" in ctx) { ctx.view.share({ cspNonce: ctx.response.nonce }); } helmetCspMiddleware(ctx.response.request, ctx.response.response, (error) => { if (error) { reject(error); } else { resolve(); } }); }); }; } // src/guards/no_sniff.ts function noSniffFactory(options) { if (!options.enabled) { return noop; } return function noSniff({ response }) { response.header("X-Content-Type-Options", "nosniff"); }; } // src/guards/frame_guard.ts var ALLOWED_ACTIONS = ["DENY", "ALLOW-FROM", "SAMEORIGIN"]; function frameGuardFactory(options) { if (!options.enabled) { return noop; } const action = options.action || "SAMEORIGIN"; const resolvedOptions = { ...options, action }; if (!ALLOWED_ACTIONS.includes(resolvedOptions.action)) { throw new Error('frameGuard: Action must be one of "DENY", "ALLOW-FROM" or "SAMEORGIGIN"'); } if (resolvedOptions.action === "ALLOW-FROM" && !resolvedOptions["domain"]) { throw new Error('frameGuard: Domain value is required when using action as "ALLOW-FROM"'); } const result = resolvedOptions.action === "ALLOW-FROM" ? `${action} ${resolvedOptions["domain"]}` : action; return function frameGuard({ response }) { response.header("X-Frame-Options", result); }; } export { hstsFactory, csrfFactory, cspFactory, noSniffFactory, frameGuardFactory };