@appwarden/middleware
Version:
Instantly shut off access your app deployed on Cloudflare or Vercel
146 lines (139 loc) • 4.84 kB
JavaScript
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
};