fix nginx security headers not being applied to https responses
nginx add_header inheritance meant X-Content-Type-Options and X-Frame-Options were silently dropped from all https server blocks. moved all security headers into a shared snippet (security-headers.inc) included per server block. added pytest-based header verification that auto-discovers sites from conf files.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""Test that security headers are present on all HTTPS sites."""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import urllib.request
|
||||
import ssl
|
||||
|
||||
import pytest
|
||||
|
||||
CONF_DIR = os.path.join(os.path.dirname(__file__), "..", "docker", "nginx", "conf.d")
|
||||
|
||||
|
||||
def discover_sites():
|
||||
"""Build HTTPS URLs from nginx conf files that include security-headers.inc."""
|
||||
sites = []
|
||||
for conf in sorted(glob.glob(os.path.join(CONF_DIR, "*.conf"))):
|
||||
text = open(conf).read()
|
||||
if "security-headers.inc" not in text:
|
||||
continue
|
||||
# Use the first server_name in the block that includes the headers
|
||||
for block in re.split(r"(?=server\s*\{)", text):
|
||||
if "security-headers.inc" in block:
|
||||
m = re.search(r"server_name\s+([^;]+);", block)
|
||||
if m:
|
||||
sites.append(f"https://{m.group(1).split()[0]}")
|
||||
break
|
||||
return sites
|
||||
|
||||
|
||||
SITES = discover_sites()
|
||||
|
||||
EXPECTED_HEADERS = {
|
||||
"Strict-Transport-Security": "max-age=63072000; preload",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Frame-Options": "DENY",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
"Permissions-Policy": "geolocation=(), microphone=(), camera=()",
|
||||
}
|
||||
|
||||
|
||||
def get_headers(url):
|
||||
ctx = ssl.create_default_context()
|
||||
req = urllib.request.Request(url, method="HEAD")
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=10)
|
||||
return dict(resp.headers)
|
||||
except urllib.error.HTTPError as e:
|
||||
return dict(e.headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url", SITES, ids=lambda u: u.split("//")[1])
|
||||
def test_security_headers(url):
|
||||
headers = get_headers(url)
|
||||
missing = []
|
||||
for name, expected in EXPECTED_HEADERS.items():
|
||||
actual = headers.get(name)
|
||||
if actual != expected:
|
||||
missing.append(f"{name}: expected '{expected}', got '{actual}'")
|
||||
assert not missing, "\n".join(missing)
|
||||
Reference in New Issue
Block a user