mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 23:02:17 +10:00
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:
@@ -33,6 +33,8 @@ describe("handleUpload", () => {
|
|||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(readMock).toHaveBeenCalledWith("uploads/user-1/pictures/photo.jpeg");
|
expect(readMock).toHaveBeenCalledWith("uploads/user-1/pictures/photo.jpeg");
|
||||||
expect(response.headers.get("Content-Type")).toBe("image/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 () => {
|
it("does not serve private agent attachment keys through the public uploads route", async () => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { basename, extname, normalize } from "node:path";
|
import { basename, extname, normalize } from "node:path";
|
||||||
import { getStorageService, inferContentType } from "@reactive-resume/api/features/storage";
|
import { getStorageService, inferContentType } from "@reactive-resume/api/features/storage";
|
||||||
import { env } from "@reactive-resume/env/server";
|
|
||||||
|
|
||||||
export async function handleUpload(request: Request) {
|
export async function handleUpload(request: Request) {
|
||||||
const { userId, filePath } = parseRouteParams(request.url);
|
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("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||||
headers.set("X-Frame-Options", "DENY");
|
headers.set("X-Frame-Options", "DENY");
|
||||||
headers.set("X-Download-Options", "noopen");
|
headers.set("X-Download-Options", "noopen");
|
||||||
headers.set("Access-Control-Allow-Origin", env.APP_URL);
|
|
||||||
|
|
||||||
return new Response(toArrayBuffer(storedFile.data), { headers });
|
return new Response(toArrayBuffer(storedFile.data), { headers });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ describe("web app fallback classification", () => {
|
|||||||
expect(await response.text()).toBe("<html>app</html>");
|
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([
|
it.each([
|
||||||
"/auth/login",
|
"/auth/login",
|
||||||
"/dashboard",
|
"/dashboard",
|
||||||
|
|||||||
@@ -52,12 +52,21 @@ function isPublicResumePath(pathname: string): boolean {
|
|||||||
return segments.length === 2 && firstSegment !== undefined && !reservedPublicResumeSegments.has(firstSegment);
|
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) {
|
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)) {
|
if (isNoindexShellPath(pathname) || isPublicResumePath(pathname)) {
|
||||||
return {
|
return {
|
||||||
"Content-Type": "text/html; charset=UTF-8",
|
"Content-Type": "text/html; charset=UTF-8",
|
||||||
"X-Robots-Tag": "noindex, follow",
|
"X-Robots-Tag": "noindex, follow",
|
||||||
|
...BASE_SECURITY_HEADERS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user