Files
wehub-resource-sync 7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

190 lines
6.6 KiB
Python

"""
Cookie Security Tests
Tests for the dynamic cookie security behavior.
Security model:
- Secure flag is added iff wsgi.url_scheme == "https".
- ProxyFix (with x_proto=1) translates X-Forwarded-Proto into wsgi.url_scheme
before SecureCookieMiddleware sees it, so requests served over HTTPS by a
reverse proxy correctly receive the Secure flag.
- HTTP requests never get Secure regardless of source IP. Setting Secure on
an HTTP response causes the browser to drop the cookie entirely, so doing
so based on IP heuristics broke legitimate Docker/LAN access without
providing any cryptographic protection (issue #3849).
- TESTING mode: Never get Secure flag (for CI/development).
"""
import pytest
from tests.test_utils import add_src_to_path
add_src_to_path()
@pytest.fixture
def app():
"""Create test application with TESTING mode enabled (default for tests)."""
from local_deep_research.web.app_factory import create_app
app, _ = create_app()
app.config["TESTING"] = True
app.config["WTF_CSRF_ENABLED"] = False
return app
@pytest.fixture
def app_production_mode():
"""Create test application with production-like cookie security."""
import os
from local_deep_research.web.app_factory import create_app
old_testing = os.environ.pop("TESTING", None)
old_ci = os.environ.pop("CI", None)
old_pytest = os.environ.pop("PYTEST_CURRENT_TEST", None)
try:
app, _ = create_app()
app.config["TESTING"] = True
app.config["WTF_CSRF_ENABLED"] = False
app.config["LDR_TESTING_MODE"] = False
app.config["PREFERRED_URL_SCHEME"] = "http"
return app
finally:
if old_testing is not None:
os.environ["TESTING"] = old_testing
if old_ci is not None:
os.environ["CI"] = old_ci
if old_pytest is not None:
os.environ["PYTEST_CURRENT_TEST"] = old_pytest
@pytest.fixture
def client(app):
"""Create test client."""
return app.test_client()
class TestLocalhostCookieSecurity:
"""Localhost HTTP works without Secure flag."""
def test_localhost_http_no_secure_flag(self, client):
response = client.get("/auth/login")
set_cookie = response.headers.get("Set-Cookie", "")
assert response.status_code == 200
assert "session=" in set_cookie
def test_localhost_session_cookie_works(self, client):
response1 = client.get("/auth/login")
assert response1.status_code == 200
response2 = client.get("/auth/login")
assert response2.status_code == 200
class TestLocalhostProductionMode:
"""Localhost HTTP works in production mode (non-testing)."""
def test_localhost_http_no_secure_flag_in_production(
self, app_production_mode
):
app = app_production_mode
with app.test_client() as client:
response = client.get("/auth/login")
set_cookie = response.headers.get("Set-Cookie", "")
assert "; Secure" not in set_cookie, (
f"Localhost HTTP should NOT have Secure flag. Got: {set_cookie}"
)
assert "session=" in set_cookie
class TestHttpCookieSecurity:
"""HTTP requests never get the Secure flag, regardless of source IP.
This is the core fix for #3849: setting Secure on an HTTP response makes
the browser drop the cookie, breaking sessions without adding any real
security (the underlying transport is still plaintext).
"""
@pytest.mark.parametrize(
"remote_addr",
[
"127.0.0.1",
"192.168.1.100",
"10.0.0.50",
"172.16.0.1",
"172.17.0.2", # Default Docker bridge
"172.67.130.145", # Docker Desktop NAT (the #3849 trigger)
"8.8.8.8",
"104.16.0.1",
],
)
def test_http_no_secure_flag(self, app_production_mode, remote_addr):
app = app_production_mode
with app.test_client() as client:
response = client.get(
"/auth/login",
environ_base={"REMOTE_ADDR": remote_addr},
)
set_cookie = response.headers.get("Set-Cookie", "")
assert "; Secure" not in set_cookie, (
f"HTTP from {remote_addr} should NOT have Secure flag. "
f"Got: {set_cookie}"
)
class TestHttpsCookieSecurity:
"""HTTPS requests always get the Secure flag."""
def test_https_via_x_forwarded_proto_gets_secure(self, app_production_mode):
"""A reverse proxy terminating HTTPS sets X-Forwarded-Proto: https.
ProxyFix translates this into wsgi.url_scheme, after which Secure is
added.
"""
app = app_production_mode
with app.test_client() as client:
response = client.get(
"/auth/login",
headers={"X-Forwarded-Proto": "https"},
environ_base={"REMOTE_ADDR": "10.0.0.1"},
)
set_cookie = response.headers.get("Set-Cookie", "")
assert "; Secure" in set_cookie, (
f"HTTPS via reverse proxy should add Secure. Got: {set_cookie}"
)
class TestTestingModeBehavior:
"""TESTING mode disables Secure flag entirely."""
def test_testing_mode_no_secure_flag(self, app):
app.config["LDR_TESTING_MODE"] = True
with app.test_client() as client:
response = client.get(
"/auth/login",
headers={"X-Forwarded-Proto": "https"},
)
set_cookie = response.headers.get("Set-Cookie", "")
assert "; Secure" not in set_cookie
@pytest.mark.skip(reason="documentation/placeholder test - not implemented")
def test_cookie_security_summary():
"""
Summary of cookie security behavior for CI validation.
Expected behavior:
| Scenario | wsgi.url_scheme | Secure Flag |
|-------------------------------|-----------------|-------------|
| HTTP from localhost | http | No |
| HTTP from LAN client | http | No |
| HTTP from Docker NAT gateway | http | No |
| HTTP from public IP | http | No |
| HTTPS via reverse proxy | https | Yes |
| Direct HTTPS | https | Yes |
| TESTING=1 mode | any | No |
The decision is based purely on the protocol (post-ProxyFix), not the
source IP. Setting Secure on HTTP responses doesn't add security and
breaks the browser's ability to store the cookie.
"""
assert True