diff options
| author | Leitwolf11 | 2026-11-06 08:05:21 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-11-06 08:05:21 +0300 |
| commit | c4fb3effa7f806c570771bb83c0ed6988006bc24 (patch) | |
| tree | 6e5e9b36161934b3e646ea21ccc00407cf29ba0f /apps/web/tests | |
| parent | 35e09214817585007308bdbc26d877791843ab29 (diff) | |
| download | progmaweb-security/content-security-policy.tar.gz | |
Give every page a Content-Security-Policy with its own noncesecurity/content-security-policy
The pages of progmasoft.com and its hosts had no content security
policy. The proxy now creates a 128-bit nonce for every request and sends
the policy both ways: on the request, so that Next marks its own inline
scripts with the nonce, and on the response. The layout reads the nonce
for the theme bootstrap. Every page is rendered on demand already, so
none is served with a stale nonce.
Scripts may come from the host, carry the nonce, or be Cloudflare's Web
Analytics beacon from static.cloudflareinsights.com, which reports to
cloudflareinsights.com. 'strict-dynamic' is left out, because browsers
ignore host sources under it and the beacon is added by Cloudflare
without a nonce. Styles allow inline ones; objects, framing and other
base URLs are refused. There is no form-action: sign-in and OAuth
consent end in redirects to other hosts, loopback addresses included,
and browsers apply form-action to them. The development server also
gets 'unsafe-eval' for its hot reload.
Tests check the nonce and each directive; ARCHITECTURE.md describes the
policy.
Diffstat (limited to 'apps/web/tests')
| -rw-r--r-- | apps/web/tests/content-security-policy.test.mjs | 54 |
1 files changed, 54 insertions, 0 deletions
diff --git a/apps/web/tests/content-security-policy.test.mjs b/apps/web/tests/content-security-policy.test.mjs new file mode 100644 index 0000000..8b7a888 --- /dev/null +++ b/apps/web/tests/content-security-policy.test.mjs @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { + contentSecurityPolicy, + createNonce, +} from "../lib/content-security-policy.ts"; + +function directives(policy) { + return new Map( + policy.split("; ").map((directive) => { + const [name, ...values] = directive.split(" "); + return [name, values]; + }), + ); +} + +test("a nonce is 128 random bits and differs on every call", () => { + const nonces = new Set(Array.from({ length: 100 }, createNonce)); + assert.equal(nonces.size, 100); + for (const nonce of nonces) { + assert.match(nonce, /^[A-Za-z0-9+/]{22}==$/); + } +}); + +test("scripts run from this host, with the nonce, or from Cloudflare's beacon", () => { + const policy = directives(contentSecurityPolicy("abc", false)); + assert.deepEqual(policy.get("script-src"), [ + "'self'", + "'nonce-abc'", + "https://static.cloudflareinsights.com", + ]); + assert.deepEqual(policy.get("connect-src"), [ + "'self'", + "https://cloudflareinsights.com", + ]); + assert.deepEqual(policy.get("object-src"), ["'none'"]); + assert.deepEqual(policy.get("frame-ancestors"), ["'none'"]); + assert.deepEqual(policy.get("base-uri"), ["'self'"]); + // OAuth ends in redirects to other hosts after a form is sent. + assert.equal(policy.has("form-action"), false); + for (const values of policy.values()) { + assert.equal(values.includes("'unsafe-eval'"), false); + assert.equal(values.includes("'strict-dynamic'"), false); + } + assert.equal(policy.get("script-src").includes("'unsafe-inline'"), false); +}); + +test("only the development server may evaluate code", () => { + const policy = directives(contentSecurityPolicy("abc", true)); + assert.ok(policy.get("script-src").includes("'unsafe-eval'")); +}); |