nuxt-security
Version:
🛡️ Security Module for Nuxt based on HTTP Headers and Middleware
73 lines (72 loc) • 3.04 kB
JavaScript
import { defineNitroPlugin } from "#imports";
import { resolveSecurityRules } from "../context/index.js";
import { generateHash } from "../../../utils/crypto";
const INLINE_SCRIPT_RE = /<script(?![^>]*?\bsrc="[\w:.\-\\/]+")[^>]*>([\s\S]*?)<\/script>/gi;
const STYLE_RE = /<style[^>]*>([\s\S]*?)<\/style>/gi;
const SCRIPT_RE = /<script(?=[^>]+\bsrc="[^"]+")(?=[^>]+\bintegrity="([\w\-+/=]+)")[^>]+(?:\/>|><\/script[^>]*?>)/gi;
const LINK_RE = /<link(?=[^>]+\brel="(stylesheet|preload|modulepreload)")(?=[^>]+\bintegrity="([\w\-+/=]+)")(?=(?:[^>]+\bas="(\w+)")?)[^>]+>/gi;
export default defineNitroPlugin((nitroApp) => {
if (!import.meta.prerender) {
return;
}
nitroApp.hooks.hook("render:html", async (html, { event }) => {
const rules = resolveSecurityRules(event);
if (!rules.enabled || !rules.headers || !rules.headers.contentSecurityPolicy) {
return;
}
event.context.security.hashes = {
script: /* @__PURE__ */ new Set(),
style: /* @__PURE__ */ new Set()
};
const scriptHashes = event.context.security.hashes.script;
const styleHashes = event.context.security.hashes.style;
const hashAlgorithm = "SHA-256";
if (rules.ssg) {
const { hashScripts, hashStyles } = rules.ssg;
const sections = ["body", "bodyAppend", "bodyPrepend", "head"];
for (const section of sections) {
for (const element of html[section]) {
if (hashScripts) {
const inlineScriptMatches = element.matchAll(INLINE_SCRIPT_RE);
for (const [, scriptText] of inlineScriptMatches) {
const hash = await generateHash(scriptText, hashAlgorithm);
scriptHashes.add(`'${hash}'`);
}
const externalScriptMatches = element.matchAll(SCRIPT_RE);
for (const [, integrity] of externalScriptMatches) {
scriptHashes.add(`'${integrity}'`);
}
}
if (hashStyles) {
const styleMatches = element.matchAll(STYLE_RE);
for (const [, styleText] of styleMatches) {
const hash = await generateHash(styleText, hashAlgorithm);
styleHashes.add(`'${hash}'`);
}
}
const linkMatches = element.matchAll(LINK_RE);
for (const [, rel, integrity, as] of linkMatches) {
if (integrity) {
if (rel === "stylesheet" && hashStyles) {
styleHashes.add(`'${integrity}'`);
} else if (rel === "preload" && hashScripts) {
switch (as) {
case "script":
case "audioworklet":
case "paintworklet":
case "xlst":
scriptHashes.add(`'${integrity}'`);
break;
default:
break;
}
} else if (rel === "modulepreload" && hashScripts) {
scriptHashes.add(`'${integrity}'`);
}
}
}
}
}
}
});
});