From f7838084023cd4dc9830036153491e1880ad00b1 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Mon, 13 Jul 2026 23:37:12 +0800 Subject: [PATCH] add auditable report renderer isolation probe --- docs/research/pre_work_error_ledger.md | 2 +- scripts/report_renderer_isolation_poc.py | 81 +++++++++++++++++++++ tests/test_report_renderer_isolation_poc.py | 9 +++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 scripts/report_renderer_isolation_poc.py create mode 100644 tests/test_report_renderer_isolation_poc.py diff --git a/docs/research/pre_work_error_ledger.md b/docs/research/pre_work_error_ledger.md index 9f0235e3..a82ea0b6 100644 --- a/docs/research/pre_work_error_ledger.md +++ b/docs/research/pre_work_error_ledger.md @@ -76,7 +76,7 @@ For large architecture or release work, also read: | ERR-043 | Localhost POST requests trusted CORS response headers as an execution guard; report Chromium could load external/local resources; async job IDs were predictable and persisted without capability authentication or TTL. | mitigated 2026-07-11 | Keep `tests/test_runtime_security_p0.py`; enforce Origin/Host/JSON, sandbox report resources, use random capability tokens, `0600` atomic records, TTL deletion and a bounded worker queue. Run an isolated Chromium network PoC before declaring the renderer fully hardened. | | ERR-044 | Focused selections that include legacy full chart API tests can still exceed the 120-second desktop command budget even after pure calculation tests pass. | observed 2026-07-11 | Keep P0 calculation/security tests pure and fast; profile the legacy chart fixture separately before using the full API file as a blocking CI gate. | | ERR-045 | Three-engine readiness was mistaken for completed same-chart parity. Public replay on 2026-07-11 captured PyJHora and jyotishganit raw, but VedAstro returned `official_snapshot_budget_exhausted` with no raw response. | active external blocker | Keep `three_engine_parity_runner.py`; status remains `blocked`/`partial` until all required raw artifacts are normalized into comparison rows. | -| ERR-046 | Report-renderer SSRF/file PoC could not run because the Playwright Chromium binary was absent and installation exceeded the desktop outer timeout. | blocked environment | Keep route/JS-denial tests; rerun isolated HTTP/file PoC only after a verified Chromium installation, then update this ledger with the measured request count. | +| ERR-046 | Report-renderer SSRF/file PoC cannot complete because the Playwright Chromium binary is unavailable. | blocked environment | Keep route/JS-denial tests and run `scripts/report_renderer_isolation_poc.py`; only `status=pass` with zero HTTP probe requests and blocked file/http resources proves isolated rendering. Latest run 2026-07-13: `blocked: chromium_unavailable:Error`. | | ERR-047 | Initial `slow` marker partition for `test_api_server_security.py` still exceeded the 120-second desktop budget; heavy paths extend beyond VedAstro/high-rigor prefix groups. | active profiling blocker | Profile test node IDs in bounded subprocess batches, mark only measured heavy tests, and keep fast-security acceptance separate from long CI integration coverage. | | ERR-048 | Candidate-time scanner assumed all documented D4/D24/D30 divisions were exposed by `jyotish_engine.py varga`; actual `--d4` failed at runtime. | mitigated 2026-07-12 | Candidate scans must record unsupported Varga flags as `unavailable_vargas`; only successfully computed D1/D9/D10 fields may drive local sensitivity output until a unified Varga contract exists. | | ERR-049 | PyJHora benchmark runner executed on `--help`, used a wrong repository-root path in `run_skill_baseline.py`, and failed when reused without pre-created output directories. | mitigated 2026-07-12 | Keep `tests/test_pyjhora_compare_cli.py`; require explicit `--build-local`, safe argparse help, correct repo root, and directory creation inside `run_sample()`. | diff --git a/scripts/report_renderer_isolation_poc.py b/scripts/report_renderer_isolation_poc.py new file mode 100644 index 00000000..e2c13e3e --- /dev/null +++ b/scripts/report_renderer_isolation_poc.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Run an isolated Chromium proof that report rendering cannot fetch external resources.""" +from __future__ import annotations + +import json +import tempfile +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Any + +try: + from report_builder import is_allowed_report_resource_url +except ImportError: + from scripts.report_builder import is_allowed_report_resource_url + + +class _ProbeHandler(BaseHTTPRequestHandler): + requests = 0 + + def do_GET(self) -> None: # noqa: N802 + type(self).requests += 1 + self.send_response(200) + self.end_headers() + + def log_message(self, _format: str, *_args: Any) -> None: + return + + +def run_poc() -> dict[str, Any]: + try: + from playwright.sync_api import sync_playwright + except ImportError: + return {"scope": "report_renderer_isolation_poc", "status": "blocked", "reason": "playwright_python_missing"} + + server = ThreadingHTTPServer(("127.0.0.1", 0), _ProbeHandler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + secret = root / "secret.txt" + secret.write_text("must-not-load", encoding="utf-8") + html = root / "report.html" + remote_url = f"http://127.0.0.1:{server.server_port}/probe" + html.write_text( + f'

report

', + encoding="utf-8", + ) + report_url = html.as_uri() + blocked: list[str] = [] + try: + with sync_playwright() as playwright: + browser = playwright.chromium.launch(headless=True) + context = browser.new_context(java_script_enabled=False) + page = context.new_page() + page.route( + "**/*", + lambda route: route.continue_() + if is_allowed_report_resource_url(route.request.url, report_url=report_url) + else (blocked.append(route.request.url), route.abort())[1], + ) + page.goto(report_url, wait_until="networkidle") + context.close() + browser.close() + except Exception as exc: # Browser binary/startup is an environment boundary. + return {"scope": "report_renderer_isolation_poc", "status": "blocked", "reason": f"chromium_unavailable:{type(exc).__name__}"} + return { + "scope": "report_renderer_isolation_poc", + "status": "pass" if _ProbeHandler.requests == 0 and len(blocked) >= 2 else "fail", + "http_probe_requests": _ProbeHandler.requests, + "blocked_resource_count": len(blocked), + "blocked_schemes": sorted({url.split(":", 1)[0] for url in blocked}), + } + finally: + server.shutdown() + server.server_close() + + +if __name__ == "__main__": + print(json.dumps(run_poc(), ensure_ascii=False, sort_keys=True)) diff --git a/tests/test_report_renderer_isolation_poc.py b/tests/test_report_renderer_isolation_poc.py new file mode 100644 index 00000000..108a6edd --- /dev/null +++ b/tests/test_report_renderer_isolation_poc.py @@ -0,0 +1,9 @@ +from scripts.report_renderer_isolation_poc import run_poc + + +def test_report_renderer_isolation_poc_never_claims_pass_without_browser() -> None: + result = run_poc() + assert result["status"] in {"pass", "fail", "blocked"} + if result["status"] == "pass": + assert result["http_probe_requests"] == 0 + assert result["blocked_resource_count"] >= 2