Files
ServerKit/backend/tests/test_security_headers.py
Juan DenisandClaude Fable 5.1 3468afe48f test: read nginx configs as UTF-8 in CSP header test
The shipped nginx configs contain box-drawing characters; a bare
read_text() falls back to cp1252 on Windows and raises UnicodeDecodeError.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVRnRXqNMZagqQ3RTqG5pz
2026-09-04 22:21:04 -04:00

89 lines
3.0 KiB
Python

"""Proving tests for the panel's security headers.
HTTPS is optional in ServerKit, so the panel must NOT advertise HSTS/preload
(a hard-to-reverse browser commitment) unless the deployment actually
terminates HTTPS. The installer records that via SSL_MODE/HSTS_ENABLED; behind
a proxy Flask can't tell real TLS from a Cloudflare-Flexible edge, so we trust
the recorded choice rather than the request scheme.
"""
from pathlib import Path
import config as cfg
import pytest
PANEL_NGINX_CONFIGS = [
Path(__file__).resolve().parents[2] / 'nginx' / 'nginx.conf',
Path(__file__).resolve().parents[2]
/ 'nginx'
/ 'sites-available'
/ 'serverkit.conf',
]
def _make_app(hsts_enabled, debug):
from flask import Flask
from app.middleware.security import register_security_headers
app = Flask(__name__)
app.debug = debug
app.config['HSTS_ENABLED'] = hsts_enabled
register_security_headers(app)
@app.route('/ping')
def ping():
return 'ok'
return app
def test_hsts_sent_in_secure_production():
r = _make_app(hsts_enabled=True, debug=False).test_client().get('/ping')
hsts = r.headers.get('Strict-Transport-Security')
assert hsts and 'preload' in hsts and 'includeSubDomains' in hsts
def test_hsts_absent_in_insecure_mode():
r = _make_app(hsts_enabled=False, debug=False).test_client().get('/ping')
assert 'Strict-Transport-Security' not in r.headers
# The non-committal hardening headers are still applied.
assert r.headers.get('X-Content-Type-Options') == 'nosniff'
assert 'Content-Security-Policy' in r.headers
def test_hsts_absent_in_debug_even_if_secure():
r = _make_app(hsts_enabled=True, debug=True).test_client().get('/ping')
assert 'Strict-Transport-Security' not in r.headers
@pytest.mark.parametrize('debug', [False, True])
def test_csp_allows_only_github_as_an_external_form_target(debug):
r = _make_app(hsts_enabled=False, debug=debug).test_client().get('/ping')
csp = r.headers['Content-Security-Policy']
form_action = next(
directive.strip()
for directive in csp.split(';')
if directive.strip().startswith('form-action ')
)
assert form_action == "form-action 'self' https://github.com"
@pytest.mark.parametrize('config_path', PANEL_NGINX_CONFIGS)
def test_panel_nginx_csp_allows_github_manifest_submission(config_path):
config = config_path.read_text(encoding='utf-8')
assert "form-action 'self' https://github.com;" in config
assert "form-action 'self';" not in config
def test_resolve_ssl_mode(monkeypatch):
monkeypatch.setenv('SERVERKIT_SSL_MODE', 'secure')
assert cfg._resolve_ssl_mode() == 'secure'
# Unrecognised value falls through; no /etc/serverkit/ssl-mode on the test
# box, so it lands on the safe default.
monkeypatch.setenv('SERVERKIT_SSL_MODE', 'bogus')
assert cfg._resolve_ssl_mode() == 'insecure'
monkeypatch.delenv('SERVERKIT_SSL_MODE', raising=False)
assert cfg._resolve_ssl_mode() == 'insecure'