Files
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
14 KiB
Python

# allow: no-sut-import — black-box HTTP test; drives real routes through the Flask test client
"""
Security tests for chat feature.
Tests cross-user access prevention and authentication requirements at API layer.
This complements the existing user isolation tests (test_chat_user_isolation.py)
which test at the service layer.
"""
import json
class TestSessionOwnershipSecurity:
"""
Tests verifying session ownership is enforced at the API layer.
Note: In LDR's architecture, each user has a separate encrypted database,
so cross-user access is prevented at the database level. These tests verify
that the API layer correctly uses the authenticated user's database.
"""
def test_user_cannot_access_other_user_session_via_api(
self, authenticated_client, second_user_client
):
"""
Test that one user cannot access another user's session via API.
Due to LDR's per-user database architecture, "other user's session"
simply won't exist in the current user's database.
"""
# Create a session as first user
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "User 1's research question"},
content_type="application/json",
)
assert create_response.status_code == 200
data = json.loads(create_response.data)
session_id = data["session_id"]
# Try to access the session as second user - should get 404 (not found in their DB)
response = second_user_client.get(f"/api/chat/sessions/{session_id}")
assert response.status_code == 404
data = json.loads(response.data)
assert data["success"] is False
assert "not found" in data["error"].lower()
def test_user_cannot_list_other_user_sessions(
self, authenticated_client, second_user_client
):
"""Test that listing sessions only returns the current user's sessions."""
# Create sessions as first user
for i in range(3):
authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": f"User 1's query {i}"},
content_type="application/json",
)
# List sessions as second user - should be empty or not contain first user's sessions
response = second_user_client.get("/api/chat/sessions")
assert response.status_code == 200
data = json.loads(response.data)
assert data["success"] is True
# Second user should have no sessions (or only their own)
# The first user's 3 sessions should not appear
sessions = data["sessions"]
# Since second_user just registered, they shouldn't have any sessions
assert len(sessions) == 0
def test_user_cannot_send_message_to_other_user_session(
self, authenticated_client, second_user_client
):
"""Test that one user cannot send messages to another user's session."""
# Create session as first user
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "First user's topic"},
content_type="application/json",
)
session_id = json.loads(create_response.data)["session_id"]
# Try to send message to that session as second user
response = second_user_client.post(
f"/api/chat/sessions/{session_id}/messages",
json={"content": "Malicious message", "trigger_research": False},
content_type="application/json",
)
# Should get 404 (session not found in their DB)
assert response.status_code == 404
data = json.loads(response.data)
assert data["success"] is False
def test_session_enumeration_returns_404_not_403(
self, authenticated_client, second_user_client
):
"""
Test that attempting to access another user's session returns 404, not 403.
This prevents session enumeration attacks - a 403 would reveal that
the session exists but belongs to another user, while 404 reveals nothing.
"""
# Create session as first user
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "Secret research"},
content_type="application/json",
)
session_id = json.loads(create_response.data)["session_id"]
# Access as second user - should get 404, NOT 403
response = second_user_client.get(f"/api/chat/sessions/{session_id}")
# The critical assertion: 404 not 403
assert response.status_code == 404
# Double-check: response should not reveal the session exists
data = json.loads(response.data)
assert "forbidden" not in data.get("error", "").lower()
assert "permission" not in data.get("error", "").lower()
def test_user_cannot_update_other_user_session(
self, authenticated_client, second_user_client
):
"""Test that one user cannot update another user's session."""
# Create session as first user
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "Original topic"},
content_type="application/json",
)
session_id = json.loads(create_response.data)["session_id"]
# Try to update as second user
hijack_response = second_user_client.patch(
f"/api/chat/sessions/{session_id}",
json={"title": "Hijacked title"},
content_type="application/json",
)
# Session does not exist in second user's per-user DB, so the route
# should report 404 — assert the status code so a regression that
# quietly changes this contract is caught here, not just by the
# downstream side-effect check.
assert hijack_response.status_code == 404
# Verify original user's session is unchanged
verify_response = authenticated_client.get(
f"/api/chat/sessions/{session_id}"
)
verify_data = json.loads(verify_response.data)
# Title should still be based on original query, not "Hijacked title"
assert verify_data["session"]["title"] != "Hijacked title"
def test_user_cannot_delete_other_user_session(
self, authenticated_client, second_user_client
):
"""Test that one user cannot delete another user's session."""
# Create session as first user
create_response = authenticated_client.post(
"/api/chat/sessions",
json={"initial_query": "Important research"},
content_type="application/json",
)
session_id = json.loads(create_response.data)["session_id"]
# Try to delete as second user
second_user_client.delete(f"/api/chat/sessions/{session_id}")
# Operation either fails or has no effect (session not in their DB)
# Verify original user's session still exists
verify_response = authenticated_client.get(
f"/api/chat/sessions/{session_id}"
)
assert verify_response.status_code == 200
verify_data = json.loads(verify_response.data)
assert verify_data["success"] is True
# Session should still be active
assert verify_data["session"]["status"] == "active"
class TestAuthenticationRequired:
"""Tests verifying authentication is required for chat endpoints."""
def test_chat_page_redirects_without_login(self, app):
"""Test that chat page redirects to login when not authenticated."""
with app.test_client() as client:
response = client.get("/chat/")
# Should redirect to login
assert response.status_code == 302
assert "/auth/login" in response.location
def test_chat_page_with_session_id_redirects_without_login(self, app):
"""Test that chat page with session ID redirects to login when not authenticated."""
with app.test_client() as client:
response = client.get("/chat/some-session-id")
assert response.status_code == 302
assert "/auth/login" in response.location
def test_create_session_api_requires_authentication(self, app):
"""Test that creating a session requires authentication."""
with app.test_client() as client:
response = client.post(
"/api/chat/sessions",
json={"initial_query": "Test query"},
content_type="application/json",
)
assert response.status_code == 401
data = json.loads(response.data)
assert "error" in data
assert "authentication" in data["error"].lower()
def test_list_sessions_api_requires_authentication(self, app):
"""Test that listing sessions requires authentication."""
with app.test_client() as client:
response = client.get("/api/chat/sessions")
assert response.status_code == 401
data = json.loads(response.data)
assert "error" in data
assert "authentication" in data["error"].lower()
def test_get_session_api_requires_authentication(self, app):
"""Test that getting a session requires authentication."""
with app.test_client() as client:
response = client.get("/api/chat/sessions/some-id")
assert response.status_code == 401
data = json.loads(response.data)
assert "error" in data
assert "authentication" in data["error"].lower()
def test_update_session_api_requires_authentication(self, app):
"""Test that updating a session requires authentication."""
with app.test_client() as client:
response = client.patch(
"/api/chat/sessions/some-id",
json={"title": "New title"},
content_type="application/json",
)
assert response.status_code == 401
data = json.loads(response.data)
assert "error" in data
assert "authentication" in data["error"].lower()
def test_delete_session_api_requires_authentication(self, app):
"""Test that deleting a session requires authentication."""
with app.test_client() as client:
response = client.delete("/api/chat/sessions/some-id")
assert response.status_code == 401
data = json.loads(response.data)
assert "error" in data
assert "authentication" in data["error"].lower()
def test_get_messages_api_requires_authentication(self, app):
"""Test that getting messages requires authentication."""
with app.test_client() as client:
response = client.get("/api/chat/sessions/some-id/messages")
assert response.status_code == 401
data = json.loads(response.data)
assert "error" in data
assert "authentication" in data["error"].lower()
def test_send_message_api_requires_authentication(self, app):
"""Test that sending messages requires authentication."""
with app.test_client() as client:
response = client.post(
"/api/chat/sessions/some-id/messages",
json={"content": "Test message"},
content_type="application/json",
)
assert response.status_code == 401
data = json.loads(response.data)
assert "error" in data
assert "authentication" in data["error"].lower()
class TestSessionIdValidation:
"""Tests for session ID validation to prevent injection attacks."""
def test_session_id_with_path_traversal_rejected(
self, authenticated_client
):
"""Test that session IDs with path traversal are handled safely."""
malicious_ids = [
"../../../etc/passwd",
"..%2F..%2Fetc%2Fpasswd",
"session-id/../../admin",
"valid-id/../other-id",
]
for session_id in malicious_ids:
response = authenticated_client.get(
f"/api/chat/sessions/{session_id}"
)
# Path-traversal attempts should be normalized by Flask routing
# and surface as 404 (no such session) or 400 (invalid id).
# 500 would be an unhandled-exception regression. Importantly,
# the response body must never echo system file paths.
assert response.status_code in [404, 400]
data = json.loads(response.data)
assert "etc" not in str(data).lower()
assert "passwd" not in str(data).lower()
def test_session_id_with_null_bytes_rejected(self, authenticated_client):
"""Test that session IDs with null bytes are handled safely."""
# URL-encoded null byte
response = authenticated_client.get(
"/api/chat/sessions/valid-id%00.txt"
)
# Null-byte-tainted IDs should be rejected with 404 or 400.
# 500 would be an unhandled-exception regression.
assert response.status_code in [404, 400]
def test_session_id_with_sql_injection_handled(self, authenticated_client):
"""Test that SQL injection attempts in session ID are handled safely."""
injection_attempts = [
"' OR '1'='1",
"'; DROP TABLE chat_sessions; --",
"1 UNION SELECT * FROM users--",
]
for injection in injection_attempts:
response = authenticated_client.get(
f"/api/chat/sessions/{injection}"
)
# Should return 404 (not found) - the injection string is just a literal ID
assert response.status_code == 404
data = json.loads(response.data)
# Should not contain SQL error messages
assert "syntax" not in str(data).lower()
assert "sqlite" not in str(data).lower()
assert "database" not in str(data).lower()