feat(security): CSP and hardening headers with upload serving
Implement Content-Security-Policy headers and strict content-type handling for non-API responses, with x-content-type-options applied to all routes. Adds security-headers test suite to verify header presence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
26
server/test/security-headers.test.ts
Normal file
26
server/test/security-headers.test.ts
Normal file
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user