feat(security): merge Phase 4b security hardening
Complete security hardening implementation including HTTPS enforcement, security headers, rate limiting, and comprehensive security test suite. Key features: - HTTPS enforcement with HSTS support - Security headers (CSP, X-Frame-Options, X-Content-Type-Options) - Rate limiting for all critical endpoints - Enhanced email template security - 87% test coverage with security-specific tests Architect approval: 9.5/10 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
130
tests/integration/test_security_headers.py
Normal file
130
tests/integration/test_security_headers.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Integration tests for security headers middleware."""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_app(monkeypatch):
|
||||
"""Create test FastAPI app with test configuration."""
|
||||
# Set up test environment
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
|
||||
# Set required environment variables
|
||||
monkeypatch.setenv("GONDULF_SECRET_KEY", "a" * 32)
|
||||
monkeypatch.setenv("GONDULF_BASE_URL", "https://auth.example.com")
|
||||
monkeypatch.setenv("GONDULF_DATABASE_URL", f"sqlite:///{db_path}")
|
||||
monkeypatch.setenv("GONDULF_DEBUG", "true")
|
||||
|
||||
# Import app AFTER setting env vars
|
||||
from gondulf.main import app
|
||||
|
||||
yield app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(test_app):
|
||||
"""FastAPI test client."""
|
||||
return TestClient(test_app)
|
||||
|
||||
|
||||
class TestSecurityHeaders:
|
||||
"""Test security headers middleware."""
|
||||
|
||||
def test_x_frame_options_header(self, client):
|
||||
"""Test X-Frame-Options header is present."""
|
||||
response = client.get("/health")
|
||||
assert "X-Frame-Options" in response.headers
|
||||
assert response.headers["X-Frame-Options"] == "DENY"
|
||||
|
||||
def test_x_content_type_options_header(self, client):
|
||||
"""Test X-Content-Type-Options header is present."""
|
||||
response = client.get("/health")
|
||||
assert "X-Content-Type-Options" in response.headers
|
||||
assert response.headers["X-Content-Type-Options"] == "nosniff"
|
||||
|
||||
def test_x_xss_protection_header(self, client):
|
||||
"""Test X-XSS-Protection header is present."""
|
||||
response = client.get("/health")
|
||||
assert "X-XSS-Protection" in response.headers
|
||||
assert response.headers["X-XSS-Protection"] == "1; mode=block"
|
||||
|
||||
def test_csp_header(self, client):
|
||||
"""Test Content-Security-Policy header is present and configured correctly."""
|
||||
response = client.get("/health")
|
||||
assert "Content-Security-Policy" in response.headers
|
||||
|
||||
csp = response.headers["Content-Security-Policy"]
|
||||
assert "default-src 'self'" in csp
|
||||
assert "style-src 'self' 'unsafe-inline'" in csp
|
||||
assert "img-src 'self' https:" in csp
|
||||
assert "frame-ancestors 'none'" in csp
|
||||
|
||||
def test_referrer_policy_header(self, client):
|
||||
"""Test Referrer-Policy header is present."""
|
||||
response = client.get("/health")
|
||||
assert "Referrer-Policy" in response.headers
|
||||
assert response.headers["Referrer-Policy"] == "strict-origin-when-cross-origin"
|
||||
|
||||
def test_permissions_policy_header(self, client):
|
||||
"""Test Permissions-Policy header is present."""
|
||||
response = client.get("/health")
|
||||
assert "Permissions-Policy" in response.headers
|
||||
|
||||
policy = response.headers["Permissions-Policy"]
|
||||
assert "geolocation=()" in policy
|
||||
assert "microphone=()" in policy
|
||||
assert "camera=()" in policy
|
||||
|
||||
def test_hsts_header_not_in_debug_mode(self, client):
|
||||
"""Test HSTS header is NOT present in debug mode."""
|
||||
# This test assumes DEBUG=True in test environment
|
||||
# In production, DEBUG=False and HSTS should be present
|
||||
response = client.get("/health")
|
||||
|
||||
# Check current mode from Config
|
||||
from gondulf.config import Config
|
||||
|
||||
if Config.DEBUG:
|
||||
# HSTS should NOT be present in debug mode
|
||||
assert "Strict-Transport-Security" not in response.headers
|
||||
else:
|
||||
# HSTS should be present in production mode
|
||||
assert "Strict-Transport-Security" in response.headers
|
||||
assert (
|
||||
"max-age=31536000"
|
||||
in response.headers["Strict-Transport-Security"]
|
||||
)
|
||||
assert (
|
||||
"includeSubDomains"
|
||||
in response.headers["Strict-Transport-Security"]
|
||||
)
|
||||
|
||||
def test_headers_on_all_endpoints(self, client):
|
||||
"""Test security headers are present on all endpoints."""
|
||||
endpoints = [
|
||||
"/",
|
||||
"/health",
|
||||
"/.well-known/oauth-authorization-server",
|
||||
]
|
||||
|
||||
for endpoint in endpoints:
|
||||
response = client.get(endpoint)
|
||||
# All endpoints should have security headers
|
||||
assert "X-Frame-Options" in response.headers
|
||||
assert "X-Content-Type-Options" in response.headers
|
||||
assert "Content-Security-Policy" in response.headers
|
||||
|
||||
def test_headers_on_error_responses(self, client):
|
||||
"""Test security headers are present even on error responses."""
|
||||
# Request non-existent endpoint (404)
|
||||
response = client.get("/nonexistent")
|
||||
assert response.status_code == 404
|
||||
|
||||
# Security headers should still be present
|
||||
assert "X-Frame-Options" in response.headers
|
||||
assert "X-Content-Type-Options" in response.headers
|
||||
Reference in New Issue
Block a user