nuxt-security
Version:
🛡️ Security Module for Nuxt based on HTTP Headers and Middleware
67 lines (66 loc) • 2.33 kB
JavaScript
import { defineNitroPlugin } from "nitropack/runtime";
import { resolveSecurityRules } from "../context/index.js";
import { generateRandomNonce } from "../../../utils/crypto";
const LINK_RE = /<link\b([^>]*?>)/gi;
const NONCE_RE = /nonce="[^"]+"/i;
const SCRIPT_RE = /<script\b([^>]*?>)/gi;
const STYLE_RE = /<style\b([^>]*?>)/gi;
const QUOTE_MASK_RE = /"([^"]*)"/g;
const QUOTE_RESTORE_RE = /__QUOTE_PLACEHOLDER_(\d+)__/g;
function injectNonceToTags(element, nonce) {
if (typeof element !== "string") {
return element;
}
const quotes = [];
let maskedElement = element.replace(QUOTE_MASK_RE, (match) => {
quotes.push(match);
return `__QUOTE_PLACEHOLDER_${quotes.length - 1}__`;
});
maskedElement = maskedElement.replace(LINK_RE, (match, rest) => {
if (NONCE_RE.test(rest)) {
return match.replace(NONCE_RE, `nonce="${nonce}"`);
}
return `<link nonce="${nonce}"` + rest;
});
maskedElement = maskedElement.replace(SCRIPT_RE, (match, rest) => {
return `<script nonce="${nonce}"` + rest;
});
maskedElement = maskedElement.replace(STYLE_RE, (match, rest) => {
return `<style nonce="${nonce}"` + rest;
});
const restoredHtml = maskedElement.replace(QUOTE_RESTORE_RE, (match, index) => {
return quotes[parseInt(index, 10)];
});
return restoredHtml;
}
export default defineNitroPlugin((nitroApp) => {
if (import.meta.prerender) {
return;
}
nitroApp.hooks.hook("request", (event) => {
if (event.context.security?.nonce) {
return;
}
const rules = resolveSecurityRules(event);
if (rules.enabled && rules.nonce && !import.meta.prerender) {
const nonce = generateRandomNonce();
event.context.security.nonce = nonce;
}
});
nitroApp.hooks.hook("render:html", (html, { event }) => {
const rules = resolveSecurityRules(event);
if (!rules.enabled || !rules.headers || !rules.headers.contentSecurityPolicy || !rules.nonce) {
return;
}
const nonce = event.context.security.nonce;
const sections = ["body", "bodyAppend", "bodyPrepend", "head"];
for (const section of sections) {
html[section] = html[section].map((element) => injectNonceToTags(element, nonce));
}
if (import.meta.dev) {
html.head.push(
`<meta property="csp-nonce" nonce="${nonce}">`
);
}
});
});