feat(server): add CSP report-only and framing headers to web responses

Add X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and a report-only CSP to every HTML fallback response via a shared constant. Remove the unconditional Access-Control-Allow-Origin header from the same-origin uploads endpoint (CORP: same-site already covers it).
This commit is contained in:
Amruth Pillai
2026-07-08 15:50:13 +02:00
parent 73daf22b2f
commit 9bde7d544f
4 changed files with 21 additions and 3 deletions
+2
View File
@@ -33,6 +33,8 @@ describe("handleUpload", () => {
expect(response.status).toBe(200);
expect(readMock).toHaveBeenCalledWith("uploads/user-1/pictures/photo.jpeg");
expect(response.headers.get("Content-Type")).toBe("image/jpeg");
expect(response.headers.get("Cross-Origin-Resource-Policy")).toBe("same-site");
expect(response.headers.get("Access-Control-Allow-Origin")).toBeNull();
});
it("does not serve private agent attachment keys through the public uploads route", async () => {
-2
View File
@@ -1,7 +1,6 @@
import { createHash } from "node:crypto";
import { basename, extname, normalize } from "node:path";
import { getStorageService, inferContentType } from "@reactive-resume/api/features/storage";
import { env } from "@reactive-resume/env/server";
export async function handleUpload(request: Request) {
const { userId, filePath } = parseRouteParams(request.url);
@@ -41,7 +40,6 @@ export async function handleUpload(request: Request) {
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
headers.set("X-Frame-Options", "DENY");
headers.set("X-Download-Options", "noopen");
headers.set("Access-Control-Allow-Origin", env.APP_URL);
return new Response(toArrayBuffer(storedFile.data), { headers });
}
+9
View File
@@ -32,6 +32,15 @@ describe("web app fallback classification", () => {
expect(await response.text()).toBe("<html>app</html>");
});
it.each(["/", "/alice/resume"])("sets framing and report-only CSP security headers on %s", async (pathname) => {
const response = await handleWebApp(new Request(`https://example.com${pathname}`));
expect(response.status).toBe(200);
expect(response.headers.get("X-Frame-Options")).toBe("DENY");
expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff");
expect(response.headers.get("Content-Security-Policy-Report-Only")).toContain("frame-ancestors 'none'");
});
it.each([
"/auth/login",
"/dashboard",
+10 -1
View File
@@ -52,12 +52,21 @@ function isPublicResumePath(pathname: string): boolean {
return segments.length === 2 && firstSegment !== undefined && !reservedPublicResumeSegments.has(firstSegment);
}
const BASE_SECURITY_HEADERS = {
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Content-Security-Policy-Report-Only":
"default-src 'self'; img-src 'self' data: blob:; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; object-src 'none'",
};
function getFallbackResponseHeaders(pathname: string) {
if (pathname === "/") return { "Content-Type": "text/html; charset=UTF-8" };
if (pathname === "/") return { "Content-Type": "text/html; charset=UTF-8", ...BASE_SECURITY_HEADERS };
if (isNoindexShellPath(pathname) || isPublicResumePath(pathname)) {
return {
"Content-Type": "text/html; charset=UTF-8",
"X-Robots-Tag": "noindex, follow",
...BASE_SECURITY_HEADERS,
};
}