@adonisjs/shield
Version:
A middleware for AdonisJS to keep web applications secure from common web attacks
275 lines (264 loc) • 7.95 kB
JavaScript
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
};