diff --git a/server/src/app.ts b/server/src/app.ts index 6a7194d..89682e0 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -8,9 +8,18 @@ import { settingsRoutes } from "./routes/settings"; export type Deps = { db: Db; key: Buffer; dataDir: string; llmFetch?: typeof fetch }; const PUBLIC = new Set(["/api/health", "/api/me", "/api/setup", "/api/login"]); +const CSP = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none'"; export function createApp(deps: Deps) { const app = new Hono(); + app.use("*", async (c, next) => { + await next(); + c.header("x-content-type-options", "nosniff"); + if (!c.req.path.startsWith("/api/")) { + c.header("content-security-policy", CSP); + c.header("referrer-policy", "no-referrer"); + } + }); app.get("/api/health", (c) => c.json({ ok: true })); app.use("/api/*", async (c, next) => { if (PUBLIC.has(c.req.path)) return next(); diff --git a/server/test/security-headers.test.ts b/server/test/security-headers.test.ts new file mode 100644 index 0000000..d9c9d13 --- /dev/null +++ b/server/test/security-headers.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createApp } from "../src/app"; +import { openDb } from "../src/db"; +import { loadOrCreateKey } from "../src/lib/crypto"; + +function makeApp() { + const dir = mkdtempSync(join(tmpdir(), "helios-")); + return createApp({ db: openDb(dir), key: loadOrCreateKey(dir), dataDir: dir }); +} + +describe("security headers", () => { + test("API responses: nosniff", async () => { + const res = await makeApp().request("/api/health"); + expect(res.headers.get("x-content-type-options")).toBe("nosniff"); + }); + test("non-API responses: CSP + nosniff + referrer policy", async () => { + const res = await makeApp().request("/anything"); + expect(res.headers.get("content-security-policy")).toContain("default-src 'self'"); + expect(res.headers.get("content-security-policy")).toContain("frame-ancestors 'none'"); + expect(res.headers.get("x-content-type-options")).toBe("nosniff"); + expect(res.headers.get("referrer-policy")).toBe("no-referrer"); + }); +});