Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 2s
- Content-Security-Policy: nonce-based per-request plug replacing unsafe-inline scripts - RemoteIp: CIDR-based trust gating via InetCidr, skips forwarded headers from untrusted peers - Rate limiting: auth pipeline (20/min), LiveView event handlers, existing mobile channel limits - NetworkPolicy: 4 k8s policies (web ingress, cluster, metrics, egress) for least-privilege networking - PartitionManager: deadlock retry with exponential backoff in drop_partition - Tests: reduced parallelism (max_cases 4), packets_test async:false to prevent trigger contention - k8s: APRS_PASSWORD -> APRS_PASSCODE secretRef, vendor/aprs submodule hardened
123 lines
4.4 KiB
Elixir
123 lines
4.4 KiB
Elixir
defmodule AprsmeWeb.Plugs.ContentSecurityPolicyTest do
|
|
use AprsmeWeb.ConnCase
|
|
|
|
alias AprsmeWeb.Plugs.ContentSecurityPolicy
|
|
|
|
describe "call/2" do
|
|
test "sets Content-Security-Policy header", %{conn: conn} do
|
|
conn = ContentSecurityPolicy.call(conn, [])
|
|
|
|
csp = conn |> get_resp_header("content-security-policy") |> List.first()
|
|
assert is_binary(csp) and byte_size(csp) > 0
|
|
|
|
# Verify required directives are present
|
|
assert csp =~ "default-src 'self'"
|
|
assert csp =~ "object-src 'none'"
|
|
assert csp =~ "frame-ancestors 'none'"
|
|
assert csp =~ "base-uri 'self'"
|
|
assert csp =~ "form-action 'self'"
|
|
end
|
|
|
|
test "includes nonce in script-src", %{conn: conn} do
|
|
conn = ContentSecurityPolicy.call(conn, [])
|
|
|
|
csp = conn |> get_resp_header("content-security-policy") |> List.first()
|
|
|
|
# Verify nonce-based script-src is used instead of unsafe-inline
|
|
assert csp =~ ~r/script-src[^;]*'nonce-[A-Za-z0-9+\/=]+'/
|
|
|
|
# Extract just the script-src directive and ensure it has no unsafe-inline
|
|
[script_src] = Regex.run(~r/script-src\s+([^;]+)/, csp, capture: :all_but_first)
|
|
refute script_src =~ "'unsafe-inline'"
|
|
end
|
|
|
|
test "stores nonce in conn.private", %{conn: conn} do
|
|
conn = ContentSecurityPolicy.call(conn, [])
|
|
|
|
nonce = conn.private[:csp_nonce]
|
|
assert is_binary(nonce) and byte_size(nonce) > 0
|
|
|
|
# Verify the private nonce matches the one in the header
|
|
csp = conn |> get_resp_header("content-security-policy") |> List.first()
|
|
assert csp =~ "'nonce-#{nonce}'"
|
|
end
|
|
|
|
test "generates a unique nonce per request", %{conn: conn} do
|
|
conn1 = ContentSecurityPolicy.call(conn, [])
|
|
conn2 = ContentSecurityPolicy.call(build_conn(), [])
|
|
|
|
nonce1 = conn1.private[:csp_nonce]
|
|
nonce2 = conn2.private[:csp_nonce]
|
|
|
|
assert nonce1 != nonce2
|
|
end
|
|
|
|
test "allows external script sources", %{conn: conn} do
|
|
conn = ContentSecurityPolicy.call(conn, [])
|
|
|
|
csp = conn |> get_resp_header("content-security-policy") |> List.first()
|
|
|
|
# Plausible analytics (self-hosted)
|
|
assert csp =~ "script-src" and csp =~ "https://a.w5isp.com"
|
|
# Sentry error tracking
|
|
assert csp =~ "script-src" and csp =~ "https://js.sentry-cdn.com"
|
|
# Leaflet/map CDN
|
|
assert csp =~ "script-src" and csp =~ "https://unpkg.com"
|
|
end
|
|
|
|
test "allows map tile connect sources", %{conn: conn} do
|
|
conn = ContentSecurityPolicy.call(conn, [])
|
|
|
|
csp = conn |> get_resp_header("content-security-policy") |> List.first()
|
|
|
|
assert csp =~ "connect-src" and csp =~ "https://tile.openstreetmap.org"
|
|
assert csp =~ "connect-src" and csp =~ "https://*.tile.openstreetmap.org"
|
|
assert csp =~ "connect-src" and csp =~ "https://*.basemaps.cartocdn.com"
|
|
assert csp =~ "connect-src" and csp =~ "wss:"
|
|
end
|
|
|
|
test "allows sentry connect sources", %{conn: conn} do
|
|
conn = ContentSecurityPolicy.call(conn, [])
|
|
|
|
csp = conn |> get_resp_header("content-security-policy") |> List.first()
|
|
|
|
assert csp =~ "connect-src" and csp =~ "https://*.ingest.sentry.io"
|
|
assert csp =~ "connect-src" and csp =~ "https://*.sentry.io"
|
|
end
|
|
|
|
test "allows image sources from any HTTPS origin for map tiles", %{conn: conn} do
|
|
conn = ContentSecurityPolicy.call(conn, [])
|
|
|
|
csp = conn |> get_resp_header("content-security-policy") |> List.first()
|
|
|
|
assert csp =~ "img-src" and csp =~ "https:"
|
|
assert csp =~ "img-src" and csp =~ "data:"
|
|
assert csp =~ "img-src" and csp =~ "blob:"
|
|
end
|
|
|
|
test "allows unsafe-inline for style-src (required by Tailwind and Phoenix)", %{conn: conn} do
|
|
conn = ContentSecurityPolicy.call(conn, [])
|
|
|
|
csp = conn |> get_resp_header("content-security-policy") |> List.first()
|
|
|
|
# style-src still needs unsafe-inline for Tailwind/Phoenix inline styles
|
|
assert csp =~ "style-src" and csp =~ "'unsafe-inline'"
|
|
end
|
|
|
|
test "sets worker-src to allow web workers and blob workers", %{conn: conn} do
|
|
conn = ContentSecurityPolicy.call(conn, [])
|
|
|
|
csp = conn |> get_resp_header("content-security-policy") |> List.first()
|
|
|
|
assert csp =~ "worker-src 'self' blob:"
|
|
end
|
|
|
|
test "blocks framing via frame-ancestors 'none'", %{conn: conn} do
|
|
conn = ContentSecurityPolicy.call(conn, [])
|
|
|
|
csp = conn |> get_resp_header("content-security-policy") |> List.first()
|
|
|
|
assert csp =~ "frame-ancestors 'none'"
|
|
end
|
|
end
|
|
end
|