nuxt-security
Version:
🛡️ Security Module for Nuxt based on HTTP Headers and Middleware
241 lines (238 loc) • 9.03 kB
JavaScript
import { defineNuxtModule, createResolver, addVitePlugin, addServerPlugin, addServerHandler, installModule, addImportsDir, addServerImports, useNitro } from '@nuxt/kit';
import { existsSync } from 'node:fs';
import { readdir, readFile } from 'node:fs/promises';
import { join, isAbsolute } from 'pathe';
import { defu } from 'defu';
import viteRemove from 'unplugin-remove/vite';
import { getHeadersApplicableToAllResources } from './utils/headers.mjs';
import { generateHash } from './utils/crypto.mjs';
import { defuReplaceArray } from './utils/merge.mjs';
import { defaultSecurityConfig } from './defaultConfig.mjs';
const module = defineNuxtModule({
meta: {
name: "nuxt-security",
configKey: "security"
},
async setup(options, nuxt) {
const resolver = createResolver(import.meta.url);
nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
const strict = options.strict || nuxt.options.security?.strict || nuxt.options.runtimeConfig.security?.strict || false;
nuxt.options.security = defuReplaceArray(
{ ...options, ...nuxt.options.security },
{
...defaultSecurityConfig(nuxt.options.devServer.url, strict)
}
);
nuxt.options.runtimeConfig.private = defu(
nuxt.options.runtimeConfig.private,
{
basicAuth: nuxt.options.security.basicAuth
}
);
delete nuxt.options.security.basicAuth;
nuxt.options.runtimeConfig.security = defu(
nuxt.options.runtimeConfig.security,
{
...nuxt.options.security
}
);
const securityOptions = nuxt.options.runtimeConfig.security;
if (!securityOptions.enabled) {
return;
}
if (securityOptions.removeLoggers) {
if (securityOptions.removeLoggers !== true) {
addVitePlugin(viteRemove(securityOptions.removeLoggers));
} else if (!nuxt.options.dev) {
if (nuxt.options.vite.build?.minify === "terser") {
nuxt.options.vite.build = defu(
{
terserOptions: { compress: { drop_console: true, drop_debugger: true } }
},
nuxt.options.vite.build
);
} else {
nuxt.options.vite.esbuild = defu(
{
drop: ["console", "debugger"]
},
nuxt.options.vite.esbuild
);
}
}
}
if (securityOptions.headers) {
const globalSecurityHeaders = getHeadersApplicableToAllResources(securityOptions.headers);
nuxt.options.nitro.routeRules = defuReplaceArray(
{ "/**": { headers: globalSecurityHeaders } },
nuxt.options.nitro.routeRules
);
}
for (const route in nuxt.options.nitro.routeRules) {
const rule = nuxt.options.nitro.routeRules[route];
if (rule.security && rule.security.headers) {
const { security: { headers } } = rule;
const routeSecurityHeaders = getHeadersApplicableToAllResources(headers);
nuxt.options.nitro.routeRules[route] = defuReplaceArray(
{ headers: routeSecurityHeaders },
rule
);
}
}
addServerPlugin(resolver.resolve("./runtime/nitro/plugins/00-routeRules"));
addServerPlugin(resolver.resolve("./runtime/nitro/plugins/20-subresourceIntegrity"));
addServerPlugin(resolver.resolve("./runtime/nitro/plugins/30-cspSsgHashes"));
addServerPlugin(resolver.resolve("./runtime/nitro/plugins/40-cspSsrNonce"));
addServerPlugin(resolver.resolve("./runtime/nitro/plugins/50-updateCsp"));
addServerPlugin(resolver.resolve("./runtime/nitro/plugins/60-recombineHtml"));
addServerPlugin(resolver.resolve("./runtime/nitro/plugins/70-securityHeaders"));
addServerPlugin(resolver.resolve("./runtime/nitro/plugins/80-hidePoweredBy"));
addServerPlugin(resolver.resolve("./runtime/nitro/plugins/90-prerenderedHeaders"));
reorderNitroPlugins(nuxt);
addServerHandler({
handler: resolver.resolve("./runtime/server/middleware/requestSizeLimiter")
});
addServerHandler({
handler: resolver.resolve("./runtime/server/middleware/corsHandler")
});
addServerHandler({
handler: resolver.resolve("./runtime/server/middleware/allowedMethodsRestricter")
});
registerRateLimiterStorage(nuxt, securityOptions);
addServerHandler({
handler: resolver.resolve("./runtime/server/middleware/rateLimiter")
});
addServerHandler({
handler: resolver.resolve("./runtime/server/middleware/xssValidator")
});
const basicAuthConfig = nuxt.options.runtimeConfig.private.basicAuth;
if (basicAuthConfig && (basicAuthConfig.enabled || basicAuthConfig?.value?.enabled)) {
addServerHandler({
route: basicAuthConfig.route || "",
handler: resolver.resolve("./runtime/server/middleware/basicAuth")
});
}
if (securityOptions.csrf) {
if (Object.keys(securityOptions.csrf).length) {
await installModule("nuxt-csurf", securityOptions.csrf);
} else {
await installModule("nuxt-csurf");
}
}
addImportsDir(resolver.resolve("./runtime/composables"));
addServerImports([{ name: "defuReplaceArray", from: resolver.resolve("./utils/merge") }]);
let sriHashes = {};
nuxt.options.nitro.virtual = defu(
{ "#sri-hashes": () => `export default ${JSON.stringify(sriHashes)}` },
nuxt.options.nitro.virtual
);
nuxt.hook("nitro:build:before", async (nitro) => {
sriHashes = await hashBundledAssets(nitro);
});
nuxt.hook("nitro:init", (nitro) => {
nitro.hooks.hook("prerender:done", async () => {
nitro.options.serverAssets.push({
baseName: "nuxt-security",
dir: createResolver(nuxt.options.buildDir).resolve("./nuxt-security")
});
const prerenderedHeaders = await nitro.storage.getItem("build:nuxt-security:headers.json") || {};
if (securityOptions.ssg && securityOptions.ssg.exportToPresets) {
const prerenderedHeadersRouteRules = Object.fromEntries(Object.entries(prerenderedHeaders).map(([route, headers]) => [route, { headers }]));
const n = useNitro();
n.options.routeRules = defuReplaceArray(
prerenderedHeadersRouteRules,
n.options.routeRules
);
}
nuxt.hooks.callHook("nuxt-security:prerenderedHeaders", prerenderedHeaders);
});
});
}
});
function registerRateLimiterStorage(nuxt, securityOptions) {
nuxt.hook("nitro:config", (config) => {
const driver = defu(
securityOptions.rateLimiter ? securityOptions.rateLimiter.driver : void 0,
{ name: "lruCache" }
);
const { name, options = {} } = driver;
config.storage = defu(
{
"#rate-limiter-storage": {
driver: name,
...options
}
},
config.storage
);
});
}
function reorderNitroPlugins(nuxt) {
nuxt.hook("nitro:init", (nitro) => {
const resolver = createResolver(import.meta.url);
const securityPluginsPrefix = resolver.resolve("./runtime/nitro/plugins");
nitro.options.plugins.sort((a, b) => {
if (a.startsWith(securityPluginsPrefix)) {
if (b.startsWith(securityPluginsPrefix)) {
return 0;
} else {
return 1;
}
} else {
if (b.startsWith(securityPluginsPrefix)) {
return -1;
} else {
return 0;
}
}
});
nitro.hooks.hook("prerender:config", (config) => {
config.plugins?.sort((a, b) => {
if (a?.startsWith(securityPluginsPrefix)) {
if (b?.startsWith(securityPluginsPrefix)) {
return 0;
} else {
return 1;
}
} else {
if (b?.startsWith(securityPluginsPrefix)) {
return -1;
} else {
return 0;
}
}
});
});
});
}
async function hashBundledAssets(nitro) {
const hashAlgorithm = "SHA-384";
const sriHashes = {};
const { cdnURL: appCdnUrl = "", baseURL: appBaseUrl } = nitro.options.runtimeConfig.app;
const publicAssets = nitro.options.publicAssets;
for (const publicAsset of publicAssets) {
const { dir, baseURL = "" } = publicAsset;
if (existsSync(dir)) {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile()) {
const path = join(dir, entry.name);
const content = await readFile(path);
const hash = await generateHash(content, hashAlgorithm);
const fullPath = join(baseURL, entry.name);
let url;
if (appCdnUrl) {
const relativePath = isAbsolute(fullPath) ? fullPath.slice(1) : fullPath;
const abdsoluteCdnUrl = appCdnUrl.endsWith("/") ? appCdnUrl : appCdnUrl + "/";
url = new URL(relativePath, abdsoluteCdnUrl).href;
} else {
url = join("/", appBaseUrl, fullPath);
}
sriHashes[url] = hash;
}
}
}
}
return sriHashes;
}
export { module as default };