2026-07-27 15:44:47 +02:00
|
|
|
"""B-43: the Caddyfile must keep sending baseline security headers. Caddy adds
|
|
|
|
|
none of these on its own, and the JWT lives in localStorage, so a regression
|
|
|
|
|
here silently reopens an XSS/clickjacking exposure with no test ever failing
|
|
|
|
|
in the Python suite (the Caddyfile isn't imported/exercised by anything else)."""
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
CADDYFILE = (Path(__file__).parent.parent.parent / "Caddyfile").read_text()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_header_block_present():
|
|
|
|
|
assert "header {" in CADDYFILE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_hsts_is_set():
|
|
|
|
|
assert "Strict-Transport-Security" in CADDYFILE
|
|
|
|
|
assert "max-age=" in CADDYFILE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_nosniff_is_set():
|
|
|
|
|
assert 'X-Content-Type-Options "nosniff"' in CADDYFILE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_frame_ancestors_are_blocked():
|
|
|
|
|
assert 'X-Frame-Options "DENY"' in CADDYFILE
|
|
|
|
|
assert "frame-ancestors 'none'" in CADDYFILE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_referrer_policy_is_set():
|
|
|
|
|
assert "Referrer-Policy" in CADDYFILE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_csp_default_src_is_self():
|
|
|
|
|
assert "Content-Security-Policy" in CADDYFILE
|
|
|
|
|
assert "default-src 'self'" in CADDYFILE
|
2026-08-03 22:14:03 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_forwarded_for_is_overwritten_with_the_real_peer(): # B-54
|
|
|
|
|
"""Caddy appends to a client-supplied X-Forwarded-For instead of replacing it,
|
|
|
|
|
so without this directive the header's first element is whatever the caller
|
|
|
|
|
claimed. app/api/client_ip.py reads the last hop and so holds on its own, but
|
|
|
|
|
this is what makes the header itself trustworthy — losing it silently weakens
|
|
|
|
|
every IP-keyed control (B-33's throttles, B-38's SSE cap)."""
|
|
|
|
|
assert "header_up X-Forwarded-For {remote_host}" in CADDYFILE
|