diff --git a/src/gateway/http-common.ts b/src/gateway/http-common.ts index 22e09254fd..a536df55a6 100644 --- a/src/gateway/http-common.ts +++ b/src/gateway/http-common.ts @@ -2,6 +2,17 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { GatewayAuthResult } from "./auth.js"; import { readJsonBody } from "./hooks.js"; +/** + * Apply baseline security headers that are safe for all response types (API JSON, + * HTML pages, static assets, SSE streams). Headers that restrict framing or set a + * Content-Security-Policy are intentionally omitted here because some handlers + * (canvas host, A2UI) serve content that may be loaded inside frames. + */ +export function setDefaultSecurityHeaders(res: ServerResponse) { + res.setHeader("X-Content-Type-Options", "nosniff"); + res.setHeader("Referrer-Policy", "no-referrer"); +} + export function sendJson(res: ServerResponse, status: number, body: unknown) { res.statusCode = status; res.setHeader("Content-Type", "application/json; charset=utf-8"); diff --git a/src/gateway/server-http.ts b/src/gateway/server-http.ts index f5dd2acaa7..3c8d1ec3d3 100644 --- a/src/gateway/server-http.ts +++ b/src/gateway/server-http.ts @@ -48,7 +48,7 @@ import { resolveHookChannel, resolveHookDeliver, } from "./hooks.js"; -import { sendGatewayAuthFailure } from "./http-common.js"; +import { sendGatewayAuthFailure, setDefaultSecurityHeaders } from "./http-common.js"; import { getBearerToken, getHeader } from "./http-utils.js"; import { isPrivateOrLoopbackAddress, resolveGatewayClientIp } from "./net.js"; import { handleOpenAiHttpRequest } from "./openai-http.js"; @@ -474,6 +474,8 @@ export function createGatewayHttpServer(opts: { }); async function handleRequest(req: IncomingMessage, res: ServerResponse) { + setDefaultSecurityHeaders(res); + // Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event. if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") { return;