- Add ex_slop credo plugin with all 40 checks - Remove DualKeyAccess patterns across codebase — atom-only access - Fix LengthInGuard, LengthComparison, ListLast, ReduceMapPut in lib/ - Disable LengthComparison for test files - Remove obvious comments and narrator comments (~50) - Add missing aliases for fully-qualified modules - Rewrite boilerplate docs in test/support - Add normalize_keys helpers at API boundaries
122 lines
4.3 KiB
Elixir
122 lines
4.3 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
|
|
|
|
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
|