diff --git a/README.md b/README.md index 593dd1340..f4879761f 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ If your company would like to sponsor Reactive Resume, email [hello@amruthpillai - Professionally designed templates - A4 and Letter size support - Customizable colors, fonts, and spacing -- Custom CSS for advanced styling +- Structured Style Rules for section and text styling **Privacy & Control** diff --git a/apps/server/src/static/uploads.test.ts b/apps/server/src/static/uploads.test.ts index 7d314393f..015c4d182 100644 --- a/apps/server/src/static/uploads.test.ts +++ b/apps/server/src/static/uploads.test.ts @@ -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 () => { diff --git a/apps/server/src/static/uploads.ts b/apps/server/src/static/uploads.ts index b3e37dfe6..5a22a8c33 100644 --- a/apps/server/src/static/uploads.ts +++ b/apps/server/src/static/uploads.ts @@ -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 }); } diff --git a/apps/server/src/static/web.test.ts b/apps/server/src/static/web.test.ts index c373ac130..868370e28 100644 --- a/apps/server/src/static/web.test.ts +++ b/apps/server/src/static/web.test.ts @@ -32,6 +32,15 @@ describe("web app fallback classification", () => { expect(await response.text()).toBe("app"); }); + 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", diff --git a/apps/server/src/static/web.ts b/apps/server/src/static/web.ts index d29167b5e..a8650864b 100644 --- a/apps/server/src/static/web.ts +++ b/apps/server/src/static/web.ts @@ -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, }; }