feat(gateway): add Permissions-Policy header to default security headers (#30186)

Merged via squash.

Prepared head SHA: 0dac89283f
Co-authored-by: habakan <12531644+habakan@users.noreply.github.com>
Co-authored-by: grp06 <1573959+grp06@users.noreply.github.com>
Reviewed-by: @grp06
This commit is contained in:
habakan
2026-03-04 09:25:39 +09:00
committed by GitHub
parent 0d97101665
commit 4b17d6d882
3 changed files with 51 additions and 0 deletions

View File

@@ -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.

View File

@@ -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());
});
});

View File

@@ -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);