Files
learningcircuit--local-deep…/tests/security/test_csrf_protection.py
T
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

329 lines
12 KiB
Python

"""
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