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
190 lines
6.6 KiB
Python
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
|