UNPKG

@appwarden/middleware

Version:

Instantly shut off access your app deployed on Cloudflare or Vercel

146 lines (139 loc) 4.84 kB
import { debug, printMessage } from "./chunk-JWUAFJ2E.js"; // src/schemas/use-content-security-policy.ts import { z as z2 } from "zod"; // src/types/csp.ts import { z } from "zod"; var stringySchema = z.union([z.array(z.string()), z.string(), z.boolean()]); var ContentSecurityPolicySchema = z.object({ "default-src": stringySchema.optional(), "script-src": stringySchema.optional(), "style-src": stringySchema.optional(), "img-src": stringySchema.optional(), "connect-src": stringySchema.optional(), "font-src": stringySchema.optional(), "object-src": stringySchema.optional(), "media-src": stringySchema.optional(), "frame-src": stringySchema.optional(), sandbox: stringySchema.optional(), "report-uri": stringySchema.optional(), "child-src": stringySchema.optional(), "form-action": stringySchema.optional(), "frame-ancestors": stringySchema.optional(), "plugin-types": stringySchema.optional(), "base-uri": stringySchema.optional(), "report-to": stringySchema.optional(), "worker-src": stringySchema.optional(), "manifest-src": stringySchema.optional(), "prefetch-src": stringySchema.optional(), "navigate-to": stringySchema.optional(), "require-sri-for": stringySchema.optional(), "block-all-mixed-content": stringySchema.optional(), "upgrade-insecure-requests": stringySchema.optional(), "trusted-types": stringySchema.optional(), "require-trusted-types-for": stringySchema.optional() }); // src/schemas/use-content-security-policy.ts var CSPDirectivesSchema = z2.union([ z2.string(), ContentSecurityPolicySchema ]); var CSPModeSchema = z2.union([ z2.literal("disabled"), z2.literal("report-only"), z2.literal("enforced") ]).optional().default("disabled"); var UseCSPInputSchema = z2.object({ mode: CSPModeSchema, directives: CSPDirectivesSchema.optional().refine( (val) => { try { if (typeof val === "string") { JSON.parse(val); } return true; } catch (error) { return false; } }, { message: "DirectivesBadParse" /* DirectivesBadParse */ } ).transform( (val) => typeof val === "string" ? JSON.parse(val) : val ) }).refine( (values) => ( // validate that directives are provided when the mode is "report-only" or "enforced" ["report-only", "enforced"].includes(values.mode) ? !!values.directives : true ), { path: ["directives"], message: "DirectivesRequired" /* DirectivesRequired */ } ); // src/utils/cloudflare/make-csp-header.ts var addNonce = (value, cspNonce) => value.replace("{{nonce}}", `'nonce-${cspNonce}'`); var makeCSPHeader = (cspNonce, directives, mode) => { const namesSeen = /* @__PURE__ */ new Set(), result = []; Object.entries(directives ?? {}).forEach(([originalName, value]) => { const name = originalName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); if (namesSeen.has(name)) { throw new Error(`${originalName} is specified more than once`); } namesSeen.add(name); if (Array.isArray(value)) { value = addNonce(value.join(" "), cspNonce); } else if (value === true) { value = ""; } if (value) { result.push(`${name} ${addNonce(value, cspNonce)}`); } else if (value !== false) { result.push(name); } }); return [ mode === "enforced" ? "Content-Security-Policy" : "Content-Security-Policy-Report-Only", result.join("; ") ]; }; // src/middlewares/use-content-security-policy.ts var AppendAttribute = (attribute, nonce) => ({ element: function(element) { element.setAttribute(attribute, nonce); } }); var useContentSecurityPolicy = (input) => { const parsedInput = UseCSPInputSchema.safeParse(input); if (!parsedInput.success) { throw parsedInput.error; } const config = parsedInput.data; return async (context, next) => { await next(); const { response } = context; if ( // if the csp is disabled !["enforced", "report-only"].includes(config.mode) ) { debug(printMessage("csp is disabled")); return; } if (response.headers.has("Content-Type") && !response.headers.get("Content-Type")?.includes("text/html")) { return; } const cspNonce = btoa(crypto.getRandomValues(new Uint32Array(2)).toString()); const [cspHeaderName, cspHeaderValue] = makeCSPHeader( cspNonce, config.directives, config.mode ); const nextResponse = new Response(response.body, response); nextResponse.headers.set(cspHeaderName, cspHeaderValue); nextResponse.headers.set("content-type", "text/html; charset=utf-8"); context.response = new HTMLRewriter().on("style", AppendAttribute("nonce", cspNonce)).on("script", AppendAttribute("nonce", cspNonce)).transform(nextResponse); }; }; export { CSPDirectivesSchema, CSPModeSchema, useContentSecurityPolicy };