"""Opus pre-release follow-up tests for stage-265 (PR #1445 extension hooks). These tests pin the defense-in-depth additions from the Opus advisor review: - `_fully_unquote_path` iterates up to 10 times (catches quadruple-encoded ..) - `_read_url_list` caps at `_MAX_URL_LIST` (32) entries - `_read_url_list` logs once per rejected URL - MIME map covers `ttf`, `otf`, `wasm` for modern font/wasm assets """ import logging from pathlib import Path from types import SimpleNamespace class FakeHandler: def __init__(self): self.status = None self.headers = {} self.sent_headers = [] self.body = bytearray() self.wfile = self def send_response(self, status): self.status = status def send_header(self, name, value): self.sent_headers.append((name, value)) def end_headers(self): pass def write(self, data): self.body.extend(data) def header(self, name): for key, value in self.sent_headers: if key.lower() == name.lower(): return value return None def test_fully_unquote_handles_quadruple_encoded(monkeypatch): """Quadruple-encoded `..` (`%2525252e%2525252e`) must collapse to literal `..` so the segment-level safety check rejects it. The original 3-iteration cap stopped at `%2e%2e` and would have accepted the URL into the validator. """ from api.extensions import _fully_unquote_path # Plain percent-encoding stops at `..` after 1 unquote assert _fully_unquote_path("/extensions/%2e%2e/api/session") == "/extensions/../api/session" # Double-encoded after 2 unquotes assert _fully_unquote_path("/extensions/%252e%252e/api/session") == "/extensions/../api/session" # Triple-encoded after 3 unquotes assert _fully_unquote_path("/extensions/%25252e%25252e/api/session") == "/extensions/../api/session" # Quadruple-encoded after 4 unquotes — the case that slipped through the # original `range(3)` and reached the validator unchanged assert _fully_unquote_path("/extensions/%2525252e%2525252e/api/session") == "/extensions/../api/session" def test_quadruple_encoded_traversal_url_now_rejected(tmp_path, monkeypatch): """End-to-end: quadruple-encoded `..` in a configured URL is rejected by the validator instead of slipping through. Pre-Opus this passed. """ root = tmp_path / "extensions" root.mkdir() monkeypatch.setenv("HERMES_WEBUI_EXTENSION_DIR", str(root)) monkeypatch.setenv( "HERMES_WEBUI_EXTENSION_SCRIPT_URLS", "/extensions/%2525252e%2525252e/api/session, /extensions/legit.js", ) from api.extensions import get_extension_config config = get_extension_config() # Only the legit URL should pass validation; the quadruple-encoded # traversal must be filtered out assert config["script_urls"] == ["/extensions/legit.js"] def test_url_list_caps_at_max(tmp_path, monkeypatch): """Configured URL lists cap at _MAX_URL_LIST entries to avoid pathological rendering when a misconfigured env var ships thousands of URLs. """ root = tmp_path / "extensions" root.mkdir() monkeypatch.setenv("HERMES_WEBUI_EXTENSION_DIR", str(root)) # Build 100 valid URLs urls = ", ".join(f"/extensions/script{i}.js" for i in range(100)) monkeypatch.setenv("HERMES_WEBUI_EXTENSION_SCRIPT_URLS", urls) from api.extensions import get_extension_config, _MAX_URL_LIST config = get_extension_config() assert len(config["script_urls"]) == _MAX_URL_LIST # First N kept (insertion order) assert config["script_urls"][0] == "/extensions/script0.js" assert config["script_urls"][-1] == f"/extensions/script{_MAX_URL_LIST - 1}.js" def test_url_list_logs_rejected_urls_once(tmp_path, monkeypatch, caplog): """A misconfigured URL must produce a one-shot warning so an admin who typos `https://...` (rejected as external) sees a signal in logs instead of just a silently-not-loading extension. """ root = tmp_path / "extensions" root.mkdir() monkeypatch.setenv("HERMES_WEBUI_EXTENSION_DIR", str(root)) monkeypatch.setenv( "HERMES_WEBUI_EXTENSION_SCRIPT_URLS", "https://evil.example.com/x.js, /extensions/legit.js", ) # Reset the per-process warning cache so the test doesn't accidentally # depend on state from other tests in the same run from api.extensions import _warned_urls _warned_urls.clear() caplog.set_level(logging.WARNING, logger="api.extensions") from api.extensions import get_extension_config config = get_extension_config() assert config["script_urls"] == ["/extensions/legit.js"] # The external URL must surface as a warning in the log assert any( "Rejected extension URL" in record.message and "evil.example.com" in record.message for record in caplog.records ) # Second call within the same process must not re-log (one-shot) caplog.clear() config2 = get_extension_config() assert config2["script_urls"] == ["/extensions/legit.js"] rejection_records = [ r for r in caplog.records if "Rejected extension URL" in r.message and "evil.example.com" in r.message ] assert rejection_records == [], ( "Repeated invalid URL should not re-log on every config read" ) def test_expanded_mime_map_serves_fonts_and_wasm(tmp_path, monkeypatch): """`ttf`, `otf`, and `wasm` extensions must serve with the right Content-Type so browsers don't reject (especially `.wasm`, which Chrome refuses to instantiate when served as `text/plain`). """ root = tmp_path / "extensions" root.mkdir() (root / "font.ttf").write_bytes(b"fake ttf binary") (root / "font.otf").write_bytes(b"fake otf binary") (root / "module.wasm").write_bytes(b"\x00asm" + b"\x01" * 8) monkeypatch.setenv("HERMES_WEBUI_EXTENSION_DIR", str(root)) from api.extensions import serve_extension_static ttf = FakeHandler() assert serve_extension_static(ttf, SimpleNamespace(path="/extensions/font.ttf")) is True assert ttf.status == 200 assert ttf.header("Content-Type") == "font/ttf" otf = FakeHandler() assert serve_extension_static(otf, SimpleNamespace(path="/extensions/font.otf")) is True assert otf.status == 200 assert otf.header("Content-Type") == "font/otf" wasm = FakeHandler() assert serve_extension_static(wasm, SimpleNamespace(path="/extensions/module.wasm")) is True assert wasm.status == 200 assert wasm.header("Content-Type") == "application/wasm" # Binary types must NOT have a charset suffix assert "charset" not in wasm.header("Content-Type")