mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-12 08:02:31 +00:00
feat: apply per-response development nonces after Vite finishes rendering HTML
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin AI
parent
589bc823ed
commit
34fbbc3c54
+7
-8
@@ -40,6 +40,13 @@ export function dashboardCspPlugin(env: Record<string, string>): Plugin {
|
||||
return {
|
||||
name: "warmbly:dashboard-csp",
|
||||
configureServer(server) {
|
||||
const transformHtml = server.transformIndexHtml.bind(server);
|
||||
server.transformIndexHtml = async (...args) => {
|
||||
const request = requests.getStore();
|
||||
const html = await transformHtml(...args);
|
||||
// Vite adds nonce attributes after user HTML hooks have finished.
|
||||
return request ? html.replaceAll(noncePlaceholder, request.nonce) : html;
|
||||
};
|
||||
server.middlewares.use((request, response, next) => {
|
||||
const nonce = randomBytes(24).toString("base64");
|
||||
const preview = request.url?.split("?")[0] === "/mail-preview.html";
|
||||
@@ -49,14 +56,6 @@ export function dashboardCspPlugin(env: Record<string, string>): Plugin {
|
||||
requests.run({ nonce }, next);
|
||||
});
|
||||
},
|
||||
transformIndexHtml: {
|
||||
order: "post",
|
||||
handler(html) {
|
||||
const request = requests.getStore();
|
||||
if (!request) return html;
|
||||
return html.replaceAll(noncePlaceholder, request.nonce);
|
||||
},
|
||||
},
|
||||
generateBundle: { order: "post", handler(_options, bundle) {
|
||||
const index = bundle["index.html"];
|
||||
if (!index || index.type !== "asset") throw new Error("CSP requires the dashboard HTML asset");
|
||||
|
||||
+28
-1
@@ -6,13 +6,40 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { build } from "vite";
|
||||
import { build, createServer } from "vite";
|
||||
import { configuredSources, dashboardCspPlugin, inlineHashes, noncePlaceholder, origin } from "../csp";
|
||||
import { dashboardCsp, mailPreviewCsp } from "../securityHeaders";
|
||||
|
||||
const directory = process.cwd();
|
||||
|
||||
describe("dashboard CSP", () => {
|
||||
it("serves final Vite HTML with a matching fresh nonce for concurrent requests", async () => {
|
||||
const temporary = mkdtempSync(path.join(tmpdir(), "warmbly-csp-dev-"));
|
||||
const server = await createServer({ root: temporary, configFile: false, html: { cspNonce: noncePlaceholder }, plugins: [dashboardCspPlugin({})], server: { host: "127.0.0.1", port: 0 }, logLevel: "silent" });
|
||||
try {
|
||||
writeFileSync(path.join(temporary, "index.html"), '<html><head><script>window.theme="dark";</script><style>body { margin: 0; }</style></head><body></body></html>');
|
||||
await server.listen();
|
||||
const address = server.httpServer!.address();
|
||||
if (!address || typeof address === "string") throw new Error("Expected local HTTP port");
|
||||
const results = await Promise.all([1, 2].map(async () => {
|
||||
const response = await fetch(`http://127.0.0.1:${address.port}/app/contacts`);
|
||||
expect(response.status).toBe(200);
|
||||
const nonce = response.headers.get("Content-Security-Policy")?.match(/'nonce-([^']+)'/)?.[1];
|
||||
const html = await response.text();
|
||||
expect(nonce).toBeTruthy();
|
||||
expect(html).not.toContain(noncePlaceholder);
|
||||
const nonces = [...html.matchAll(/nonce="([^"]+)"/g)].map((match) => match[1]);
|
||||
expect(nonces.length).toBeGreaterThanOrEqual(3);
|
||||
expect(new Set(nonces)).toEqual(new Set([nonce]));
|
||||
return nonce;
|
||||
}));
|
||||
expect(results[0]).not.toBe(results[1]);
|
||||
} finally {
|
||||
await server.close();
|
||||
rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("generates hashes from Vite's final HTML asset", async () => {
|
||||
const temporary = mkdtempSync(path.join(tmpdir(), "warmbly-csp-build-"));
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user