"""Pinned patch and live-process security tests for the WebUI HUX BFF.""" from __future__ import annotations from contextlib import contextmanager from email.message import Message import hashlib import hmac import http.client from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import importlib.util import io from pathlib import Path import shutil import sys import threading import time from types import ModuleType, SimpleNamespace import pytest ROOT = Path(__file__).parents[2] PATCHER = ROOT / "dockerfiles/hermes-webui-hux-bff-patch.py" FIXTURE = ROOT / "testing/fixtures/hermes-webui-0.52.181" FIXTURE_ROUTES_SHA = "099f961f023342a3284719de024ad7c2e767e7c84f0fbfd6d7240d792ebf0903" TEST_CONTEXT_KEY = b"k" * 32 TEST_SUBJECT = "usr_" + hmac.new(TEST_CONTEXT_KEY, b"hux.subject.id.v1\0slot-3", hashlib.sha256).hexdigest() def load_patcher(name: str = "hermes_hux_bff_patch"): spec = importlib.util.spec_from_file_location(name, PATCHER) assert spec and spec.loader module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def fixture_tree(tmp_path: Path) -> Path: target = tmp_path / "webui" shutil.copytree(FIXTURE, target) routes = target / "api/routes.py" source = routes.read_text() pinned_post = ' """Pinned POST route anchors for the Atlas voice patch."""' upstream_post = ' """Handle all POST routes. Returns True if handled, False for 404."""' assert source.count(pinned_post) == 1 source = source.replace(pinned_post, upstream_post, 1) for method in ("PUT", "PATCH", "DELETE"): source += (f"\n\ndef handle_{method.lower()}(handler, parsed) -> bool:\n" f' """Handle all {method} routes. Returns True if handled, False for 404."""\n' " return False\n") routes.write_text(source) return target class UpstreamHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" status = 200 response_headers = {"Content-Type": "application/json"} response_body = b'{"ok":true}' delay = 0.0 requests: list[dict] = [] def _run(self): length = int(self.headers.get("Content-Length") or 0) body = self.rfile.read(length) if length else b"" type(self).requests.append({"method": self.command, "path": self.path, "headers": dict(self.headers.items()), "body": body}) if type(self).delay: time.sleep(type(self).delay) self.send_response(type(self).status) headers = dict(type(self).response_headers) if "Content-Length" not in headers: headers["Content-Length"] = str(len(type(self).response_body)) for name, value in headers.items(): self.send_header(name, value) self.end_headers() self.wfile.write(type(self).response_body) do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = _run def log_message(self, *_args): return class ReusableServer(ThreadingHTTPServer): allow_reuse_address = True daemon_threads = True @contextmanager def upstream(**values): previous = {name: getattr(UpstreamHandler, name) for name in ("status", "response_headers", "response_body", "delay")} UpstreamHandler.requests = [] for name, value in values.items(): setattr(UpstreamHandler, name, value) server = ReusableServer(("127.0.0.1", 8790), UpstreamHandler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() try: yield UpstreamHandler finally: server.shutdown() server.server_close() thread.join(timeout=2) for name, value in previous.items(): setattr(UpstreamHandler, name, value) def auth_modules(monkeypatch, *, info=None, valid_cookie="valid.cookie", valid_csrf="csrf-value"): package = ModuleType("api") package.__path__ = [] auth = ModuleType("api.auth") auth.CSRF_HEADER_NAME = "X-Hermes-CSRF-Token" auth.parse_cookie = lambda handler: valid_cookie if "session=valid" in handler.headers.get("Cookie", "") else None auth.verify_session = lambda cookie: cookie == valid_cookie auth.ensure_trusted_auth_session = lambda _handler: info or { "auth_type": "trusted", "username": "slot-3"} auth.verify_csrf_token = lambda cookie, token: cookie == valid_cookie and token == valid_csrf package.auth = auth monkeypatch.setitem(sys.modules, "api", package) monkeypatch.setitem(sys.modules, "api.auth", auth) def relay_key(module, tmp_path: Path, value="relay_key_0123456789abcdef0123456789abcdef") -> Path: directory = tmp_path / "webui-only" directory.mkdir(mode=0o700) directory.chmod(0o700) key = directory / "relay-key" key.write_text(value) key.chmod(0o400) module.RELAY_KEY_FILE = key context_key = directory / "context-key" context_key.write_bytes(TEST_CONTEXT_KEY) context_key.chmod(0o600) module.CONTEXT_KEY_FILE = context_key return key @contextmanager def bff_server(module): class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def _run(self): from urllib.parse import urlsplit module.proxy_hux(self, urlsplit(self.path), self.command) do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = _run def log_message(self, *_args): return server = ReusableServer(("127.0.0.1", 0), Handler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() try: yield server.server_address[1] finally: server.shutdown() server.server_close() thread.join(timeout=2) def request(port: int, method="GET", path="/hux/v1/capabilities", body=None, headers=None): connection = http.client.HTTPConnection("127.0.0.1", port, timeout=3) base = {"Cookie": "session=valid", "X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": TEST_SUBJECT} base.update(headers or {}) base = {name: value for name, value in base.items() if value is not None} connection.request(method, path, body=body, headers=base) response = connection.getresponse() payload = response.read() result = response.status, dict(response.getheaders()), payload connection.close() return result def test_pinned_fixture_patch_is_atomic_and_installs_all_methods(tmp_path: Path): module = load_patcher("hux_bff_fixture") assert hashlib.sha256((FIXTURE / "api/routes.py").read_bytes()).hexdigest() == FIXTURE_ROUTES_SHA assert module.PINNED_UPSTREAM_COMMIT == "7a94e34a6d639576576baa9131acf6765f6d2b98" target = fixture_tree(tmp_path) module.apply(target) routes = (target / "api/routes.py").read_text() for method in ("GET", "POST", "PUT", "PATCH", "DELETE"): assert routes.count(f'return proxy_hux(handler, parsed, "{method}")') == 1 installed = target / "api/hux_bff.py" assert installed.read_text() == PATCHER.read_text() with pytest.raises(SystemExit, match="already installed"): module.apply(target) def test_pinned_drift_fails_before_writing_any_file(tmp_path: Path): module = load_patcher("hux_bff_drift") target = fixture_tree(tmp_path) routes = target / "api/routes.py" routes.write_text(routes.read_text().replace("Handle all PATCH routes", "Changed PATCH routes")) before = routes.read_bytes() with pytest.raises(SystemExit, match="patch context changed"): module.apply(target) assert routes.read_bytes() == before assert not (target / "api/hux_bff.py").exists() def test_target_body_and_header_validators_fail_closed(): module = load_patcher("hux_bff_validators") assert module._header_values(SimpleNamespace(), "Missing") == [] assert module._header_values(SimpleNamespace(headers={}), "Missing") == [] assert module._header_values(SimpleNamespace(headers={"Present": 7}), "Present") == ["7"] assert module._safe_target(SimpleNamespace(path="/hux/v1/memory", query="status=active&limit=20")) == \ "/hux/v1/memory?status=active&limit=20" assert module._safe_target(SimpleNamespace(path="/hux/v1/search", query="q=kidney%20stone")) == \ "/hux/v1/search?q=kidney+stone" bad_targets = [SimpleNamespace(path="/api/memory", query=""), SimpleNamespace(path="/hux/v1/../memory", query=""), SimpleNamespace(path="/hux/v1/memory", query="x=1&x=2"), SimpleNamespace(path="/hux/v1/memory", query="=empty"), SimpleNamespace(path="/hux/v1/memory", query="x=%2Fetc"), SimpleNamespace(path="/hux/v1/memory", query="not-a-pair"), SimpleNamespace(path="/hux/v1/memory", query="x=" + "a" * 4097)] for target in bad_targets: with pytest.raises(module.BffError) as error: module._safe_target(target) assert error.value.status == 400 def fake(headers, body=b""): return SimpleNamespace(headers=headers, rfile=io.BytesIO(body), path="/hux/v1/memory") valid = Message() valid["Content-Length"] = "2" valid["Content-Type"] = "application/json" assert module._read_request(fake(valid, b"{}"), "POST") == b"{}" class ShortReader(io.BytesIO): def read(self, size=-1): return super().read(min(size, 1)) assert module._read_request(SimpleNamespace(headers=valid, rfile=ShortReader(b"{}")), "POST") == b"{}" with pytest.raises(module.BffError, match="Incomplete"): module._read_request(fake(valid, b"{"), "POST") empty = Message() assert module._read_request(fake(empty), "GET") is None cases = [] for name, value in (("Transfer-Encoding", "chunked"), ("Content-Length", "nope")): headers = Message() headers[name] = value cases.append((headers, "POST", 400)) duplicate = Message() duplicate["Content-Length"] = "1" duplicate["Content-Length"] = "1" cases.append((duplicate, "POST", 400)) get_body = Message() get_body["Content-Length"] = "1" get_body["Content-Type"] = "application/json" cases.append((get_body, "GET", 400)) huge = Message() huge["Content-Length"] = str(module.MAX_REQUEST_BYTES + 1) cases.append((huge, "POST", 413)) wrong_type = Message() wrong_type["Content-Length"] = "1" wrong_type["Content-Type"] = "text/plain" cases.append((wrong_type, "POST", 415)) for headers, method, status in cases: with pytest.raises(module.BffError) as error: module._read_request(fake(headers, b"x"), method) assert error.value.status == status assert module._request_limit("/hux/v1/artifacts", "POST") == module.MAX_ARTIFACT_REQUEST_BYTES assert module._request_limit("/hux/v1/artifacts/art_1234/versions", "POST") == \ module.MAX_ARTIFACT_REQUEST_BYTES assert module._request_limit("/hux/v1/memory", "POST") == module.MAX_REQUEST_BYTES assert module._response_limit("/hux/v1/artifacts/art_1234/versions/1") == \ module.MAX_ARTIFACT_RESPONSE_BYTES assert module._response_limit("/hux/v1/artifacts/art_1234/versions/2/diff?from=1") == \ module.MAX_ARTIFACT_RESPONSE_BYTES def test_relay_key_requires_private_owned_regular_file(tmp_path: Path): module = load_patcher("hux_bff_key") key = relay_key(module, tmp_path) assert module._secure_relay_key().startswith("relay_key_") key.chmod(0o440) with pytest.raises(module.BffError, match="unavailable"): module._secure_relay_key() key.chmod(0o400) key.parent.chmod(0o750) with pytest.raises(module.BffError): module._secure_relay_key() key.parent.chmod(0o700) key.chmod(0o600) key.write_bytes(b"\xff" * 40) key.chmod(0o400) with pytest.raises(module.BffError): module._secure_relay_key() key.chmod(0o600) key.write_text("short") key.chmod(0o400) with pytest.raises(module.BffError): module._secure_relay_key() module.RELAY_KEY_FILE = tmp_path / "missing" / "key" with pytest.raises(module.BffError): module._secure_relay_key() def test_subject_is_server_derived_from_exact_context_key(tmp_path: Path): module = load_patcher("hux_bff_subject") relay_key(module, tmp_path) assert module._derived_subject("slot-3") == TEST_SUBJECT assert module._derived_subject("slot-4") != TEST_SUBJECT module.CONTEXT_KEY_FILE.chmod(0o440) with pytest.raises(module.BffError, match="identity"): module._derived_subject("slot-3") module.CONTEXT_KEY_FILE = tmp_path / "missing" with pytest.raises(module.BffError, match="identity"): module._derived_subject("slot-3") def test_live_get_injects_only_server_identity_and_safe_headers(monkeypatch, tmp_path: Path): module = load_patcher("hux_bff_live_get") auth_modules(monkeypatch) relay_key(module, tmp_path) upstream_headers = {"Content-Type": "application/vnd.hermes.hux+json; version=1", "ETag": '"3"', "HUX-Replayed": "true", "HUX-Audit-Stale": "false", "Content-Disposition": "attachment", "Set-Cookie": "upstream=bad"} with upstream(response_headers=upstream_headers), bff_server(module) as port: status, headers, body = request(port, path="/hux/v1/memory?limit=2", headers={ "Authorization": "Bearer oauth", "X-Hux-Relay-Key": "browser-forgery", "X-Hux-Trust": "worker", "X-Hux-Subject": None, "If-Match": "3", "Idempotency-Key": "browser:key:0001", "Cookie": "session=valid; oauth=secret"}) assert status == 200 and body == b'{"ok":true}' assert headers["Cache-Control"] == "no-store" assert headers["X-Content-Type-Options"] == "nosniff" assert headers["ETag"] == '"3"' and headers["HUX-Replayed"] == "true" assert headers["HUX-Audit-Stale"] == "false" and headers["Content-Disposition"] == "attachment" assert "Set-Cookie" not in headers and "Access-Control-Allow-Origin" not in headers captured = UpstreamHandler.requests[0] assert captured["headers"]["X-Hux-Subject"] == TEST_SUBJECT assert captured["headers"]["X-Hux-Trust"] == "relay" assert captured["headers"]["X-Hux-Surface"] == "chat" assert captured["headers"]["X-Hermes-Tenant-Identity"] == "slot-3" assert captured["headers"]["X-Hux-Relay-Key"].startswith("relay_key_") lowered = {name.lower() for name in captured["headers"]} assert not ({"authorization", "cookie", "x-hermes-csrf-token"} & lowered) @pytest.mark.parametrize("headers,info", [ ({"Cookie": "bad", "X-Hermes-Tenant-Identity": "slot-3"}, None), ({"Cookie": "session=valid", "X-Hermes-Tenant-Identity": "slot-3"}, {"auth_type": "local", "username": "slot-3"}), ({"Cookie": "session=valid", "X-Hermes-Tenant-Identity": "slot-4"}, {"auth_type": "trusted", "username": "slot-3"}), ({"Cookie": "session=valid", "X-Hermes-Tenant-Identity": "tenant"}, {"auth_type": "trusted", "username": "tenant"}), ({"Cookie": "session=valid", "X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_too_short"}, {"auth_type": "trusted", "username": "slot-3"}), ]) def test_live_get_requires_exact_trusted_session(monkeypatch, tmp_path: Path, headers, info): module = load_patcher("hux_bff_auth_" + hashlib.sha1(repr(headers).encode()).hexdigest()) auth_modules(monkeypatch, info=info) relay_key(module, tmp_path) with upstream(), bff_server(module) as port: status, response_headers, _body = request(port, headers=headers) assert status == 401 assert response_headers["Cache-Control"] == "no-store" assert not UpstreamHandler.requests def test_live_mutation_requires_csrf_and_preserves_valid_concurrency(monkeypatch, tmp_path: Path): module = load_patcher("hux_bff_mutation") auth_modules(monkeypatch) relay_key(module, tmp_path) payload = b'{"kind":"fact","identity":{"subject":"browser"}}' with upstream(), bff_server(module) as port: status, _, _ = request(port, method="POST", path="/hux/v1/memory", body=payload, headers={"Content-Type": "application/json"}) assert status == 403 and not UpstreamHandler.requests status, _, _ = request(port, method="POST", path="/hux/v1/memory", body=payload, headers={ "Content-Type": "application/json", "X-Hermes-CSRF-Token": "csrf-value", "If-Match": "7", "Idempotency-Key": "memory:key:0001"}) assert status == 200 captured = UpstreamHandler.requests[0] assert captured["method"] == "POST" and captured["body"] == payload assert captured["headers"]["If-Match"] == "7" assert captured["headers"]["Idempotency-Key"] == "memory:key:0001" def test_live_invalid_concurrency_headers_and_methods_never_reach_hux(monkeypatch, tmp_path: Path): module = load_patcher("hux_bff_invalid_headers") auth_modules(monkeypatch) relay_key(module, tmp_path) with upstream(), bff_server(module) as port: for name, value in (("If-Match", '"bad"'), ("Idempotency-Key", "short"), ("Last-Event-ID", "1 OR 1")): status, _, _ = request(port, headers={name: value}) assert status == 400 status, _, _ = request(port, method="POST", body=b"{}", headers={ "Content-Type": "application/json", "X-Hermes-CSRF-Token": "csrf-value", "Last-Event-ID": "2"}) assert status == 400 handler = SimpleNamespace(headers=Message(), path="/hux/v1/memory", rfile=io.BytesIO(), wfile=io.BytesIO()) handler.send_response = lambda status: setattr(handler, "status", status) handler.send_header = lambda *_args: None handler.end_headers = lambda: None assert module.proxy_hux(handler, SimpleNamespace(path="/hux/v1/memory", query=""), "TRACE") assert handler.status == 405 assert not UpstreamHandler.requests def test_live_response_bounds_redirects_content_types_and_outage(monkeypatch, tmp_path: Path): module = load_patcher("hux_bff_response_guards") auth_modules(monkeypatch) relay_key(module, tmp_path) with upstream(status=302, response_headers={"Content-Type": "application/json", "Location": "https://evil.test"}), \ bff_server(module) as port: status, headers, _ = request(port) assert status == 502 and "Location" not in headers with upstream(response_headers={"Content-Type": "text/html"}), bff_server(module) as port: assert request(port)[0] == 502 module.MAX_RESPONSE_BYTES = 8 with upstream(response_body=b"x" * 9), bff_server(module) as port: assert request(port)[0] == 502 with bff_server(module) as port: assert request(port)[0] == 502 def test_live_sse_is_bounded_uncached_and_preserves_numeric_cursor(monkeypatch, tmp_path: Path): module = load_patcher("hux_bff_sse") auth_modules(monkeypatch) relay_key(module, tmp_path) event = b"id: 2\nevent: run.completed\ndata: {}\n\n" with upstream(response_headers={"Content-Type": "text/event-stream"}, response_body=event), \ bff_server(module) as port: status, headers, body = request(port, path="/hux/v1/conversations/conv_alpha/events/stream", headers={"Last-Event-ID": "1"}) assert status == 200 and body == event assert headers["Cache-Control"] == "no-store" and headers["Connection"] == "close" assert UpstreamHandler.requests[0]["headers"]["Last-Event-ID"] == "1" module.MAX_STREAM_BYTES = 5 with upstream(response_headers={"Content-Type": "text/event-stream"}, response_body=event), \ bff_server(module) as port: assert request(port, path="/hux/v1/conversations/conv_alpha/events/stream")[2] == b"" def test_patch_is_small_isolated_and_wired_once(): source = PATCHER.read_text() assert len(source.splitlines()) < 500 assert "127.0.0.1" in source and "UPSTREAM_PORT = 8790" in source assert "HTTPConnection(UPSTREAM_HOST, UPSTREAM_PORT" in source assert "requests." not in source and "urlopen" not in source assert "Authorization" not in source and '"Cookie"' not in source assert "X-Hux-Relay-Key" in source and "0o400" in source and "O_NOFOLLOW" in source dockerfile = (ROOT / "dockerfiles/Dockerfile.hermes-webui").read_text() assert dockerfile.count(f"COPY dockerfiles/{PATCHER.name} /tmp/{PATCHER.name}") == 1 assert dockerfile.count(f"python /tmp/{PATCHER.name}") == 1