""" CSRF (Cross-Site Request Forgery) Protection Tests Tests that verify CSRF protection is properly implemented using Flask-WTF CSRF tokens for state-changing operations. """ import pytest from tests.test_utils import add_src_to_path add_src_to_path() class TestCSRFProtection: """Test CSRF protection in web forms and API endpoints.""" @pytest.fixture def app(self): """Create a test Flask app instance.""" from local_deep_research.web.app import create_app app, _ = create_app() # Unpack tuple (app, socket_service) app.config["TESTING"] = True # Enable CSRF for these tests app.config["WTF_CSRF_ENABLED"] = True app.config["WTF_CSRF_CHECK_DEFAULT"] = True app.config["SECRET_KEY"] = "test-secret-key-for-csrf" return app @pytest.fixture def client(self, app): """Create a test client.""" return app.test_client() @pytest.fixture def client_no_csrf(self): """Create a test client with CSRF disabled for comparison.""" from local_deep_research.web.app import create_app app, _ = create_app() # Unpack tuple (app, socket_service) app.config["TESTING"] = True app.config["WTF_CSRF_ENABLED"] = False return app.test_client() def test_csrf_token_endpoint_exists(self, client): """Test that CSRF token endpoint is available.""" response = client.get("/auth/csrf-token") assert response.status_code == 200 data = response.get_json() assert "csrf_token" in data assert len(data["csrf_token"]) > 0 def test_csrf_token_is_unique_per_session(self, app): """Test that each session gets a unique CSRF token.""" with app.test_client() as client1: response1 = client1.get("/auth/csrf-token") token1 = response1.get_json()["csrf_token"] with app.test_client() as client2: response2 = client2.get("/auth/csrf-token") token2 = response2.get_json()["csrf_token"] # Different sessions should have different tokens assert token1 != token2 def test_post_request_without_csrf_token_rejected(self, client): """Test that POST requests without CSRF token are rejected when CSRF is enabled.""" # Try to submit a form without CSRF token response = client.post( "/auth/login", data={"username": "testuser", "password": "testpass"}, follow_redirects=False, ) # Should be rejected (400 Bad Request or redirect with error) # Flask-WTF typically returns 400 for missing CSRF token assert response.status_code == 400, response.status_code def test_post_request_with_invalid_csrf_token_rejected(self, client): """Test that POST requests with invalid CSRF token are rejected.""" # Try to submit with fake/invalid CSRF token response = client.post( "/auth/login", data={ "username": "testuser", "password": "testpass", "csrf_token": "invalid-fake-token-12345", }, follow_redirects=False, ) # Should be rejected assert response.status_code == 400, response.status_code def test_post_request_with_valid_csrf_token_accepted(self, client): """Test that POST requests with valid CSRF token are processed.""" # Get a valid CSRF token csrf_response = client.get("/auth/csrf-token") csrf_token = csrf_response.get_json()["csrf_token"] # Submit request with valid CSRF token response = client.post( "/auth/login", data={ "username": "testuser", "password": "testpass", "csrf_token": csrf_token, }, follow_redirects=False, ) # Should not be rejected due to CSRF (which would be 400 with CSRF error message) # May return other status codes for invalid credentials, but not CSRF-related 400 # If it's 400, check it's not a CSRF error if response.status_code == 400: # Check if it's a CSRF error specifically response_data = response.get_data(as_text=True) # CSRF errors typically contain "CSRF" in the response assert "csrf" not in response_data.lower(), ( f"CSRF validation failed even with valid token: {response_data}" ) def test_csrf_token_in_json_requests(self, client): """Test CSRF protection for JSON API requests.""" # Get CSRF token # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE). csrf_response = client.get("/auth/csrf-token") csrf_token = csrf_response.get_json()["csrf_token"] # Try POST request without CSRF token in headers client.post( "/api/v1/research", json={"query": "test query"}, content_type="application/json", ) # Try POST request with CSRF token in headers client.post( "/api/v1/research", json={"query": "test query"}, headers={"X-CSRFToken": csrf_token}, content_type="application/json", ) # Without token might be rejected (depending on implementation) # With token should be processed (may have other validation) # This documents expected behavior # Note: Some APIs may exempt certain endpoints from CSRF # (e.g., if using token-based auth instead of cookies) def test_csrf_token_changes_on_regeneration(self, client): """Test that CSRF tokens can be regenerated.""" # Get first token response1 = client.get("/auth/csrf-token") token1 = response1.get_json()["csrf_token"] # Get second token (same session) response2 = client.get("/auth/csrf-token") token2 = response2.get_json()["csrf_token"] # Tokens should be stable within same session # Or may regenerate on each request (implementation dependent) assert isinstance(token1, str) assert isinstance(token2, str) def test_csrf_protection_on_state_changing_operations(self, client_no_csrf): """Test that state-changing operations require CSRF protection.""" # State-changing operations that should require CSRF: # - Login/Logout # - Creating research # - Deleting research # - Updating settings # - Any POST, PUT, DELETE, PATCH requests # Safe operations (no CSRF needed): # - GET requests (should be idempotent) # - HEAD, OPTIONS requests # Test that GET requests don't require CSRF get_response = client_no_csrf.get("/") assert get_response.status_code in [200, 302, 404] # Should work # POST should ideally require CSRF (tested above) # This is a documentation test assert True @pytest.mark.skip(reason="documentation/placeholder test - not implemented") def test_csrf_token_not_leaked_in_logs_or_urls(self): """Test that CSRF tokens are not leaked in logs or URLs.""" # CSRF tokens should: # 1. Not appear in URL query parameters (use POST body or headers) # 2. Not be logged to console or log files # 3. Not be exposed in error messages # 4. Be transmitted over HTTPS only in production # This is a security best practice documentation test # CSRF tokens should be in: # - Hidden form fields # - Request headers (X-CSRFToken) # - Request body (for form submissions) # CSRF tokens should NOT be in: # - URL query parameters (e.g., ?csrf=token) # - Referer headers # - Log files assert True # Documentation test def test_double_submit_cookie_pattern(self, client): """Test double-submit cookie CSRF protection pattern (if implemented).""" # Double-submit cookie pattern: # 1. Server sets CSRF token in cookie # 2. Client must include same token in request header/body # 3. Server verifies cookie matches header/body # Flask-WTF uses session-based CSRF tokens by default # This test documents the CSRF protection mechanism # Get CSRF token response = client.get("/auth/csrf-token") token = response.get_json()["csrf_token"] # Token should be associated with session assert token is not None assert len(token) > 0 def test_csrf_protection_exempt_endpoints(self, client): """Test that some endpoints may be exempt from CSRF protection.""" # Some endpoints may be intentionally exempt from CSRF: # - Health check endpoint # - Webhook endpoints (verified by other means) # - Public read-only APIs # Health check should work without CSRF response = client.get("/api/v1/health") assert response.status_code == 200 # This documents that certain endpoints don't need CSRF assert True class TestCSRFProtectionDocumentation: """Documentation tests for CSRF protection strategy.""" @pytest.mark.skip(reason="documentation/placeholder test - not implemented") def test_csrf_protection_strategy_documentation(self): """ Document CSRF protection strategy for LDR. CSRF Protection Mechanisms: 1. Flask-WTF CSRF tokens for web forms 2. Token validation on all state-changing operations (POST/PUT/DELETE) 3. CSRF token available via /auth/csrf-token endpoint for API clients 4. Tokens tied to user session How CSRF Works: 1. Attacker tricks victim into visiting malicious site 2. Malicious site sends forged request to legitimate site 3. Request uses victim's cookies (auto-sent by browser) 4. Without CSRF protection, legitimate site executes unwanted action CSRF Protection: - Require CSRF token with each state-changing request - Token is tied to user's session - Attacker cannot obtain victim's token (same-origin policy) - Forged requests without valid token are rejected LDR-Specific Considerations: - Local/self-hosted tool: CSRF risk is lower than multi-user SaaS - Still important for web interface security - API clients must obtain CSRF token before making requests Protected Operations: - User authentication (login/logout) - Research creation/deletion - Settings updates - Any database modifications Exempt Operations: - Read-only GET requests - Public API endpoints (if using API key auth instead) - Health checks """ assert True # Documentation test @pytest.mark.skip(reason="documentation/placeholder test - not implemented") def test_csrf_vs_cors_clarification(self): """ Clarify difference between CSRF and CORS. CSRF (Cross-Site Request Forgery): - Attack: Malicious site makes unauthorized requests on behalf of user - Protection: CSRF tokens, SameSite cookies - Scope: Protects against forged state-changing requests CORS (Cross-Origin Resource Sharing): - Feature: Allows controlled access to resources from different origins - Protection: Controls which external sites can make requests - Scope: Browser security policy for cross-origin requests Both are needed for comprehensive web security. """ assert True # Documentation test @pytest.mark.skip(reason="documentation/placeholder test - not implemented") def test_csrf_integration_with_authentication(): """ Test that CSRF protection works correctly with authentication. CSRF and Authentication: - CSRF protects authenticated users from forged requests - Attacker cannot forge requests even with victim's session cookie - CSRF token is separate from authentication token/session - Both are required for state-changing authenticated operations Authentication Flow with CSRF: 1. User authenticates (gets session cookie) 2. User obtains CSRF token 3. User makes authenticated request with both session and CSRF token 4. Server validates both authentication and CSRF token 5. Only then is request processed This provides defense-in-depth security. """ assert True # Documentation test