diff --git a/CHANGELOG.md b/CHANGELOG.md index 36040c434f1..84b8b6fbd6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/security default response headers: add `Permissions-Policy: camera=(), microphone=(), geolocation=()` to baseline gateway HTTP security headers for all responses. (#30186) thanks @habakan. - Security/auth labels: remove token and API-key snippets from user-facing auth status labels so `/status` and `/models` do not expose credential fragments. (#33262) thanks @cu1ch3n. - Security/audit denyCommands guidance: suggest likely exact node command IDs for unknown `gateway.nodes.denyCommands` entries so ineffective denylist entries are easier to correct. (#29713) thanks @liquidhorizon88-bot. - Docs/security hardening guidance: document Docker `DOCKER-USER` + UFW policy and add cross-linking from Docker install docs for VPS/public-host setups. (#27613) thanks @dorukardahan. diff --git a/src/gateway/http-common.test.ts b/src/gateway/http-common.test.ts new file mode 100644 index 00000000000..3292baed8c4 --- /dev/null +++ b/src/gateway/http-common.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { setDefaultSecurityHeaders } from "./http-common.js"; +import { makeMockHttpResponse } from "./test-http-response.js"; + +describe("setDefaultSecurityHeaders", () => { + it("sets X-Content-Type-Options", () => { + const { res, setHeader } = makeMockHttpResponse(); + setDefaultSecurityHeaders(res); + expect(setHeader).toHaveBeenCalledWith("X-Content-Type-Options", "nosniff"); + }); + + it("sets Referrer-Policy", () => { + const { res, setHeader } = makeMockHttpResponse(); + setDefaultSecurityHeaders(res); + expect(setHeader).toHaveBeenCalledWith("Referrer-Policy", "no-referrer"); + }); + + it("sets Permissions-Policy", () => { + const { res, setHeader } = makeMockHttpResponse(); + setDefaultSecurityHeaders(res); + expect(setHeader).toHaveBeenCalledWith( + "Permissions-Policy", + "camera=(), microphone=(), geolocation=()", + ); + }); + + it("sets Strict-Transport-Security when provided", () => { + const { res, setHeader } = makeMockHttpResponse(); + setDefaultSecurityHeaders(res, { + strictTransportSecurity: "max-age=63072000; includeSubDomains; preload", + }); + expect(setHeader).toHaveBeenCalledWith( + "Strict-Transport-Security", + "max-age=63072000; includeSubDomains; preload", + ); + }); + + it("does not set Strict-Transport-Security when not provided", () => { + const { res, setHeader } = makeMockHttpResponse(); + setDefaultSecurityHeaders(res); + expect(setHeader).not.toHaveBeenCalledWith("Strict-Transport-Security", expect.anything()); + }); + + it("does not set Strict-Transport-Security for empty string", () => { + const { res, setHeader } = makeMockHttpResponse(); + setDefaultSecurityHeaders(res, { strictTransportSecurity: "" }); + expect(setHeader).not.toHaveBeenCalledWith("Strict-Transport-Security", expect.anything()); + }); +}); diff --git a/src/gateway/http-common.ts b/src/gateway/http-common.ts index 7e0b84ab5d7..fdbf70b3594 100644 --- a/src/gateway/http-common.ts +++ b/src/gateway/http-common.ts @@ -14,6 +14,7 @@ export function setDefaultSecurityHeaders( ) { res.setHeader("X-Content-Type-Options", "nosniff"); res.setHeader("Referrer-Policy", "no-referrer"); + res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()"); const strictTransportSecurity = opts?.strictTransportSecurity; if (typeof strictTransportSecurity === "string" && strictTransportSecurity.length > 0) { res.setHeader("Strict-Transport-Security", strictTransportSecurity);