From 496b34fe4d0d10a25c235ebbfd8513b85bcc489e Mon Sep 17 00:00:00 2001 From: Frank Song Date: Mon, 18 May 2026 07:27:31 +0800 Subject: [PATCH] Fix CSRF test isolation --- tests/test_auth_password_hash_cache.py | 2 ++ tests/test_issue1909_csrf_token.py | 34 ++++++++++++++++++++++++++ tests/test_sprint29.py | 18 ++++++++++++-- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/tests/test_auth_password_hash_cache.py b/tests/test_auth_password_hash_cache.py index fe0f9e10..4aeab1f3 100644 --- a/tests/test_auth_password_hash_cache.py +++ b/tests/test_auth_password_hash_cache.py @@ -261,6 +261,8 @@ class TestPasswordCacheInvalidation(unittest.TestCase): def tearDown(self): if self._backup is not None: self._sf.write_text(self._backup, encoding='utf-8') + elif self._sf.exists(): + self._sf.unlink() auth._invalidate_password_hash_cache() os.environ.pop('HERMES_WEBUI_PASSWORD', None) diff --git a/tests/test_issue1909_csrf_token.py b/tests/test_issue1909_csrf_token.py index 8b98ef8b..63d4eadf 100644 --- a/tests/test_issue1909_csrf_token.py +++ b/tests/test_issue1909_csrf_token.py @@ -68,6 +68,40 @@ def test_authenticated_same_origin_browser_post_requires_session_csrf_token(monk auth._sessions.pop("c" * 64, None) +def test_authenticated_allowed_public_origin_accepts_valid_csrf_token(monkeypatch): + cookie = _signed_cookie("f" * 64) + token = auth.csrf_token_for_session(cookie) + monkeypatch.setattr(auth, "is_auth_enabled", lambda: True) + monkeypatch.setenv("HERMES_WEBUI_ALLOWED_ORIGINS", "https://myapp.example.com:8000") + try: + headers = { + "Origin": "https://myapp.example.com:8000", + "Host": "proxy.internal", + "Cookie": f"{auth.COOKIE_NAME}={cookie}", + auth.CSRF_HEADER_NAME: token, + } + assert routes._check_csrf(_FakeHandler(headers)) + finally: + auth._sessions.pop("f" * 64, None) + + +def test_authenticated_reverse_proxy_same_origin_accepts_valid_csrf_token(monkeypatch): + cookie = _signed_cookie("g" * 64) + token = auth.csrf_token_for_session(cookie) + monkeypatch.setattr(auth, "is_auth_enabled", lambda: True) + try: + headers = { + "Origin": "https://example.com", + "Host": "127.0.0.1:8787", + "X-Forwarded-Host": "example.com:443", + "Cookie": f"{auth.COOKIE_NAME}={cookie}", + auth.CSRF_HEADER_NAME: token, + } + assert routes._check_csrf(_FakeHandler(headers)) + finally: + auth._sessions.pop("g" * 64, None) + + def test_non_browser_mcp_style_authenticated_post_remains_compatible(monkeypatch): cookie = _signed_cookie("d" * 64) monkeypatch.setattr(auth, "is_auth_enabled", lambda: True) diff --git a/tests/test_sprint29.py b/tests/test_sprint29.py index 9a6e0da1..20916903 100644 --- a/tests/test_sprint29.py +++ b/tests/test_sprint29.py @@ -18,6 +18,7 @@ Covers: import importlib import json import pathlib +import pytest import sys import time import urllib.error @@ -62,6 +63,21 @@ def get_raw_with_headers(path): class TestCSRF: + @pytest.fixture(autouse=True) + def _disable_auth_for_origin_unit_checks(self, monkeypatch): + """Keep origin/port CSRF checks isolated from auth stateful tests. + + These Sprint 29 cases predate session-bound CSRF tokens and exercise + only Origin/Referer/Host allow/deny behavior. If an earlier test leaves + password auth enabled in the shared pytest process, _check_csrf also + requires a valid session CSRF token and these origin-only assertions + become order-dependent. Auth-enabled token coverage lives in + test_issue1909_csrf_token.py. + """ + import api.auth as auth + + monkeypatch.setattr(auth, "is_auth_enabled", lambda: False) + @staticmethod def _csrf_allowed(headers): from types import SimpleNamespace @@ -748,8 +764,6 @@ class TestENVLock: # ── Fixture ──────────────────────────────────────────────────────────────── -import pytest - @pytest.fixture(scope="module") def webui_server():