diff --git a/README.md b/README.md index 24de2b1..3e101df 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,9 @@ campo estruturado `retryAt` no corpo. Todas as respostas preservam esses contratos enquanto aplicam proteção uniforme contra MIME sniffing, referrer excessivo, objetos incorporados e permissões de sensores, câmera, captura de tela, localização, microfone, pagamentos, USB e -realidade estendida. O compartilhamento nativo continua permitido somente para +realidade estendida. A política de conteúdo e o cabeçalho de compatibilidade +também impedem que o observatório seja enquadrado por outra página, reduzindo o +risco de clickjacking. O compartilhamento nativo continua permitido somente para a própria origem. A metadata social aceita somente a origem oficial ou loopback explícito, sem refletir hosts encaminhados desconhecidos. diff --git a/lib/security-headers.ts b/lib/security-headers.ts index f967257..e7b8dba 100644 --- a/lib/security-headers.ts +++ b/lib/security-headers.ts @@ -16,11 +16,19 @@ export const SECURITY_PERMISSIONS_POLICY = [ "web-share=(self)", ].join(", "); +export const SECURITY_CONTENT_POLICY = [ + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + "object-src 'none'", +].join("; "); + export const SECURITY_RESPONSE_HEADERS = { - "Content-Security-Policy": "base-uri 'self'; form-action 'self'; object-src 'none'", + "Content-Security-Policy": SECURITY_CONTENT_POLICY, "Permissions-Policy": SECURITY_PERMISSIONS_POLICY, "Referrer-Policy": "strict-origin-when-cross-origin", "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", "X-Permitted-Cross-Domain-Policies": "none", } as const; diff --git a/package-lock.json b/package-lock.json index 6653e6e..12c1eee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6398,9 +6398,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", diff --git a/tests/rendered-html.test.mjs b/tests/rendered-html.test.mjs index 8547f55..a2698b9 100644 --- a/tests/rendered-html.test.mjs +++ b/tests/rendered-html.test.mjs @@ -44,6 +44,7 @@ test("server-renders the finished Constellation experience", async () => { assert.equal(response.status, 200); assert.match(response.headers.get("content-type") ?? "", /^text\/html\b/i); assert.equal(response.headers.get("x-content-type-options"), "nosniff"); + assert.equal(response.headers.get("x-frame-options"), "DENY"); assert.equal(response.headers.get("referrer-policy"), "strict-origin-when-cross-origin"); assert.equal( response.headers.get("permissions-policy"), @@ -52,7 +53,7 @@ test("server-renders the finished Constellation experience", async () => { assert.equal(response.headers.get("x-constellation-request-id"), null); assert.equal( response.headers.get("content-security-policy"), - "base-uri 'self'; form-action 'self'; object-src 'none'", + "base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'", ); const html = await response.text(); diff --git a/tests/security-headers.test.mjs b/tests/security-headers.test.mjs index e381766..010107c 100644 --- a/tests/security-headers.test.mjs +++ b/tests/security-headers.test.mjs @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { DISABLED_BROWSER_CAPABILITIES, + SECURITY_CONTENT_POLICY, SECURITY_PERMISSIONS_POLICY, SECURITY_RESPONSE_HEADERS, withSecurityHeaders, @@ -28,6 +29,11 @@ test("denies unused browser capabilities while preserving same-origin sharing", assert.match(SECURITY_PERMISSIONS_POLICY, /(?:^|, )web-share=\(self\)$/); }); +test("prevents the application from being framed by another document", () => { + assert.match(SECURITY_CONTENT_POLICY, /(?:^|; )frame-ancestors 'none'(?:;|$)/); + assert.equal(SECURITY_RESPONSE_HEADERS["X-Frame-Options"], "DENY"); +}); + test("adds baseline browser protections without losing response metadata or body", async () => { const secured = withSecurityHeaders(new Response("constellation", { status: 202,