""" Security regression tests — M5: Security Hardening and Upload Defense. Verifies that: - Secure headers are configured in all settings profiles. - CSP directives are present and restrictive. - DRF throttle classes and rates are wired up. - The SecurityAuditMiddleware is in the middleware stack. - The security middleware correctly identifies events to log. """ import importlib import os import pytest from unittest.mock import patch, MagicMock # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _load_settings(module_path: str): env_patch = { "DJANGO_SECRET_KEY": "test-secret-key", "DJANGO_ALLOWED_HOSTS": "localhost", "DATABASE_URL": "postgres://training:training@localhost:5432/training", "REDIS_URL": "redis://localhost:6379/0", "AWS_STORAGE_BUCKET_NAME": "test-bucket", "EMAIL_HOST": "smtp.example.com", "EMAIL_HOST_USER": "user@example.com", "EMAIL_HOST_PASSWORD": "secret", "DEFAULT_FROM_EMAIL": "noreply@example.com", } with patch.dict(os.environ, env_patch, clear=False): return importlib.import_module(module_path) # --------------------------------------------------------------------------- # Secure-header settings # --------------------------------------------------------------------------- @pytest.mark.security class TestSecureHeaderSettings: def test_x_frame_options_deny(self): mod = _load_settings("config.settings.base") assert mod.X_FRAME_OPTIONS == "DENY" def test_content_type_nosniff(self): mod = _load_settings("config.settings.base") assert mod.SECURE_CONTENT_TYPE_NOSNIFF is True def test_referrer_policy(self): mod = _load_settings("config.settings.base") assert mod.SECURE_REFERRER_POLICY == "strict-origin-when-cross-origin" def test_prod_hsts_configured(self): mod = _load_settings("config.settings.prod") assert mod.SECURE_HSTS_SECONDS >= 31536000 def test_prod_hsts_include_subdomains(self): mod = _load_settings("config.settings.prod") assert mod.SECURE_HSTS_INCLUDE_SUBDOMAINS is True def test_prod_hsts_preload(self): mod = _load_settings("config.settings.prod") assert mod.SECURE_HSTS_PRELOAD is True def test_prod_ssl_redirect(self): mod = _load_settings("config.settings.prod") assert mod.SECURE_SSL_REDIRECT is True def test_prod_session_cookie_secure(self): mod = _load_settings("config.settings.prod") assert mod.SESSION_COOKIE_SECURE is True def test_prod_session_cookie_httponly(self): mod = _load_settings("config.settings.prod") assert mod.SESSION_COOKIE_HTTPONLY is True def test_prod_csrf_cookie_secure(self): mod = _load_settings("config.settings.prod") assert mod.CSRF_COOKIE_SECURE is True def test_prod_csrf_cookie_httponly(self): mod = _load_settings("config.settings.prod") assert mod.CSRF_COOKIE_HTTPONLY is True # --------------------------------------------------------------------------- # CSP settings # --------------------------------------------------------------------------- @pytest.mark.security class TestCSPSettings: def test_csp_default_src_self_only(self): mod = _load_settings("config.settings.base") assert "'self'" in mod.CSP_DEFAULT_SRC def test_csp_frame_src_none(self): mod = _load_settings("config.settings.base") assert "'none'" in mod.CSP_FRAME_SRC def test_csp_object_src_none(self): mod = _load_settings("config.settings.base") assert "'none'" in mod.CSP_OBJECT_SRC def test_csp_base_uri_none(self): mod = _load_settings("config.settings.base") assert "'none'" in mod.CSP_BASE_URI def test_csp_form_action_self(self): mod = _load_settings("config.settings.base") assert "'self'" in mod.CSP_FORM_ACTION def test_csp_middleware_in_stack(self): mod = _load_settings("config.settings.base") assert "csp.middleware.CSPMiddleware" in mod.MIDDLEWARE # --------------------------------------------------------------------------- # Middleware ordering # --------------------------------------------------------------------------- @pytest.mark.security class TestMiddlewareStack: def test_security_audit_middleware_present(self): mod = _load_settings("config.settings.base") assert "api.middleware.SecurityAuditMiddleware" in mod.MIDDLEWARE def test_security_middleware_first_position(self): """Django SecurityMiddleware should be the outermost middleware.""" mod = _load_settings("config.settings.base") assert mod.MIDDLEWARE[0] == "django.middleware.security.SecurityMiddleware" def test_csrf_middleware_present(self): mod = _load_settings("config.settings.base") assert "django.middleware.csrf.CsrfViewMiddleware" in mod.MIDDLEWARE # --------------------------------------------------------------------------- # DRF Throttling # --------------------------------------------------------------------------- @pytest.mark.security class TestThrottleSettings: def test_throttle_classes_configured(self): mod = _load_settings("config.settings.base") classes = mod.REST_FRAMEWORK.get("DEFAULT_THROTTLE_CLASSES", []) assert len(classes) >= 2 def test_anon_throttle_rate_set(self): mod = _load_settings("config.settings.base") rates = mod.REST_FRAMEWORK.get("DEFAULT_THROTTLE_RATES", {}) assert "anon" in rates def test_user_throttle_rate_set(self): mod = _load_settings("config.settings.base") rates = mod.REST_FRAMEWORK.get("DEFAULT_THROTTLE_RATES", {}) assert "user" in rates def test_login_throttle_rate_set(self): mod = _load_settings("config.settings.base") rates = mod.REST_FRAMEWORK.get("DEFAULT_THROTTLE_RATES", {}) assert "login" in rates def test_upload_throttle_rate_set(self): mod = _load_settings("config.settings.base") rates = mod.REST_FRAMEWORK.get("DEFAULT_THROTTLE_RATES", {}) assert "upload" in rates def test_throttle_classes_importable(self): from api.throttles import LoginRateThrottle, UploadRateThrottle, AbuseRateThrottle assert LoginRateThrottle.scope == "login" assert UploadRateThrottle.scope == "upload" assert AbuseRateThrottle.scope == "abuse" # --------------------------------------------------------------------------- # SecurityAuditMiddleware logic # --------------------------------------------------------------------------- @pytest.mark.security class TestSecurityAuditMiddleware: def _make_request(self, path="/api/v1/auth/login/", method="POST"): request = MagicMock() request.path = path request.method = method request.META = {"REMOTE_ADDR": "10.0.0.1"} request.user = MagicMock(pk=None) return request def _make_response(self, status_code): response = MagicMock() response.status_code = status_code return response def test_401_is_security_event(self): from api.middleware import SecurityAuditMiddleware req = self._make_request(path="/api/v1/courses/") res = self._make_response(401) assert SecurityAuditMiddleware._is_security_event(req, res) is True def test_403_is_security_event(self): from api.middleware import SecurityAuditMiddleware req = self._make_request(path="/api/v1/courses/") res = self._make_response(403) assert SecurityAuditMiddleware._is_security_event(req, res) is True def test_429_is_security_event(self): from api.middleware import SecurityAuditMiddleware req = self._make_request(path="/api/v1/courses/") res = self._make_response(429) assert SecurityAuditMiddleware._is_security_event(req, res) is True def test_400_on_auth_path_is_security_event(self): from api.middleware import SecurityAuditMiddleware req = self._make_request(path="/api/v1/auth/login/") res = self._make_response(400) assert SecurityAuditMiddleware._is_security_event(req, res) is True def test_200_on_regular_path_is_not_security_event(self): from api.middleware import SecurityAuditMiddleware req = self._make_request(path="/api/v1/courses/") res = self._make_response(200) assert SecurityAuditMiddleware._is_security_event(req, res) is False def test_400_on_regular_path_is_not_security_event(self): from api.middleware import SecurityAuditMiddleware req = self._make_request(path="/api/v1/courses/") res = self._make_response(400) assert SecurityAuditMiddleware._is_security_event(req, res) is False def test_client_ip_from_forwarded_header(self): from api.middleware import SecurityAuditMiddleware req = MagicMock() req.META = {"HTTP_X_FORWARDED_FOR": "1.2.3.4, 10.0.0.1"} assert SecurityAuditMiddleware._client_ip(req) == "1.2.3.4" def test_client_ip_fallback_to_remote_addr(self): from api.middleware import SecurityAuditMiddleware req = MagicMock() req.META = {"REMOTE_ADDR": "5.6.7.8"} assert SecurityAuditMiddleware._client_ip(req) == "5.6.7.8"