20a0e43a11
- Fix test count in SECURITY_AUDIT.md (11 tests, not 24) - Add deprecation note for ALLOW-FROM in X-Frame-Options - Update documentation to recommend CSP frame-ancestors instead Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
190 lines
7.4 KiB
Python
190 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
|
|
"""
|
|
Tests for security headers middleware.
|
|
|
|
These tests validate that security headers are properly added to HTTP responses
|
|
based on configuration settings.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_security_headers_enabled_by_default(client):
|
|
"""Test that security headers are enabled by default."""
|
|
response = client.get("/")
|
|
|
|
# Should get a valid response (200, 302 redirect, or 404)
|
|
assert response.status_code in [200, 302, 404], f"Unexpected status code: {response.status_code}"
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_hsts_header_present(client):
|
|
"""Test that HSTS header is present when enabled."""
|
|
from app.config import settings
|
|
|
|
# Skip test if HSTS is disabled
|
|
if not settings.security_headers_enabled or not settings.security_header_hsts_enabled:
|
|
pytest.skip("HSTS header is disabled in configuration")
|
|
|
|
response = client.get("/")
|
|
assert "Strict-Transport-Security" in response.headers
|
|
assert "max-age" in response.headers["Strict-Transport-Security"]
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_csp_header_present(client):
|
|
"""Test that CSP header is present when enabled."""
|
|
from app.config import settings
|
|
|
|
# Skip test if CSP is disabled
|
|
if not settings.security_headers_enabled or not settings.security_header_csp_enabled:
|
|
pytest.skip("CSP header is disabled in configuration")
|
|
|
|
response = client.get("/")
|
|
assert "Content-Security-Policy" in response.headers
|
|
assert "default-src" in response.headers["Content-Security-Policy"]
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_x_frame_options_header_present(client):
|
|
"""Test that X-Frame-Options header is present when enabled."""
|
|
from app.config import settings
|
|
|
|
# Skip test if X-Frame-Options is disabled
|
|
if not settings.security_headers_enabled or not settings.security_header_x_frame_options_enabled:
|
|
pytest.skip("X-Frame-Options header is disabled in configuration")
|
|
|
|
response = client.get("/")
|
|
assert "X-Frame-Options" in response.headers
|
|
assert response.headers["X-Frame-Options"] in ["DENY", "SAMEORIGIN"]
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_x_content_type_options_header_present(client):
|
|
"""Test that X-Content-Type-Options header is present when enabled."""
|
|
from app.config import settings
|
|
|
|
# Skip test if X-Content-Type-Options is disabled
|
|
if not settings.security_headers_enabled or not settings.security_header_x_content_type_options_enabled:
|
|
pytest.skip("X-Content-Type-Options header is disabled in configuration")
|
|
|
|
response = client.get("/")
|
|
assert "X-Content-Type-Options" in response.headers
|
|
assert response.headers["X-Content-Type-Options"] == "nosniff"
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_security_headers_on_api_endpoints(client):
|
|
"""Test that security headers are applied to API endpoints."""
|
|
from app.config import settings
|
|
|
|
if not settings.security_headers_enabled:
|
|
pytest.skip("Security headers are disabled in configuration")
|
|
|
|
response = client.get("/api/diagnostic/health")
|
|
|
|
# Check that at least some security headers are present
|
|
security_headers = [
|
|
"Strict-Transport-Security",
|
|
"Content-Security-Policy",
|
|
"X-Frame-Options",
|
|
"X-Content-Type-Options",
|
|
]
|
|
present_headers = [h for h in security_headers if h in response.headers]
|
|
assert len(present_headers) > 0, "No security headers found on API endpoint"
|
|
|
|
|
|
@pytest.mark.security
|
|
def test_hsts_header_value_format(client):
|
|
"""Test that HSTS header has correct format."""
|
|
from app.config import settings
|
|
|
|
if not settings.security_headers_enabled or not settings.security_header_hsts_enabled:
|
|
pytest.skip("HSTS header is disabled in configuration")
|
|
|
|
response = client.get("/")
|
|
if "Strict-Transport-Security" in response.headers:
|
|
hsts_value = response.headers["Strict-Transport-Security"]
|
|
assert "max-age=" in hsts_value, "HSTS header missing max-age directive"
|
|
# Extract max-age value
|
|
parts = hsts_value.split(";")
|
|
max_age_part = [p.strip() for p in parts if p.strip().startswith("max-age=")]
|
|
assert len(max_age_part) > 0, "HSTS header missing max-age value"
|
|
|
|
|
|
@pytest.mark.security
|
|
def test_csp_header_value_format(client):
|
|
"""Test that CSP header has correct format."""
|
|
from app.config import settings
|
|
|
|
if not settings.security_headers_enabled or not settings.security_header_csp_enabled:
|
|
pytest.skip("CSP header is disabled in configuration")
|
|
|
|
response = client.get("/")
|
|
if "Content-Security-Policy" in response.headers:
|
|
csp_value = response.headers["Content-Security-Policy"]
|
|
# CSP should have at least a default-src directive
|
|
assert "default-src" in csp_value or "script-src" in csp_value, "CSP header missing required directives"
|
|
|
|
|
|
@pytest.mark.security
|
|
def test_x_frame_options_valid_value(client):
|
|
"""Test that X-Frame-Options header has valid value."""
|
|
from app.config import settings
|
|
|
|
if not settings.security_headers_enabled or not settings.security_header_x_frame_options_enabled:
|
|
pytest.skip("X-Frame-Options header is disabled in configuration")
|
|
|
|
response = client.get("/")
|
|
if "X-Frame-Options" in response.headers:
|
|
x_frame_value = response.headers["X-Frame-Options"]
|
|
valid_values = ["DENY", "SAMEORIGIN"]
|
|
# Note: ALLOW-FROM is deprecated in modern browsers; use CSP frame-ancestors instead
|
|
assert x_frame_value in valid_values or x_frame_value.startswith(
|
|
"ALLOW-FROM"
|
|
), f"Invalid X-Frame-Options value: {x_frame_value}"
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_security_headers_configuration_loading():
|
|
"""Test that security header configuration is loaded correctly."""
|
|
from app.config import settings
|
|
|
|
# Verify that security header configuration attributes exist
|
|
assert hasattr(settings, "security_headers_enabled")
|
|
assert hasattr(settings, "security_header_hsts_enabled")
|
|
assert hasattr(settings, "security_header_hsts_value")
|
|
assert hasattr(settings, "security_header_csp_enabled")
|
|
assert hasattr(settings, "security_header_csp_value")
|
|
assert hasattr(settings, "security_header_x_frame_options_enabled")
|
|
assert hasattr(settings, "security_header_x_frame_options_value")
|
|
assert hasattr(settings, "security_header_x_content_type_options_enabled")
|
|
|
|
# Verify that boolean settings are actual booleans
|
|
assert isinstance(settings.security_headers_enabled, bool)
|
|
assert isinstance(settings.security_header_hsts_enabled, bool)
|
|
assert isinstance(settings.security_header_csp_enabled, bool)
|
|
assert isinstance(settings.security_header_x_frame_options_enabled, bool)
|
|
assert isinstance(settings.security_header_x_content_type_options_enabled, bool)
|
|
|
|
# Verify that string settings are actual strings
|
|
assert isinstance(settings.security_header_hsts_value, str)
|
|
assert isinstance(settings.security_header_csp_value, str)
|
|
assert isinstance(settings.security_header_x_frame_options_value, str)
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_middleware_respects_configuration():
|
|
"""Test that middleware respects individual header enable/disable settings."""
|
|
from app.config import settings
|
|
from app.middleware.security_headers import SecurityHeadersMiddleware
|
|
|
|
# Create middleware instance
|
|
middleware = SecurityHeadersMiddleware(app=None, config=settings)
|
|
|
|
# Verify that middleware stores configuration
|
|
assert middleware.config == settings
|
|
assert middleware.enabled == settings.security_headers_enabled
|