fix(browser): guard Camofox snapshot/vision/images on private pages

Follow-up to #56874, which added the Camofox private-page SSRF guard
(_camofox_current_page_private_url) but wired it only into the Camofox
eval path (_camofox_eval). The other Camofox content-read tools —
camofox_snapshot, camofox_get_images, and camofox_vision — still read the
current page's accessibility tree / images / screenshot without the
guard, so on a non-local Camofox backend they can return the content of
an intranet or cloud-metadata page (e.g. 169.254.169.254) that the
terminal itself can't reach.

Apply the same guard, gated on _eval_ssrf_guard_active (non-local
backend, not a local sidecar, allow_private_urls unset) and fail-open on
probe failure, matching the eval-path guard and the main-browser
snapshot/vision guards. camofox_back is intentionally not changed: its
target is unknown until navigation completes, and the subsequent content
read is already guarded.

Adds regression tests covering the three read tools blocking on a private
page, the public-page pass-through, and the guard-inactive no-probe path.
This commit is contained in:
Evo
2026-07-02 17:35:07 +08:00
committed by kshitij
parent 0a2d4a6eea
commit a4a562ff0c
2 changed files with 165 additions and 0 deletions
+46
View File
@@ -570,6 +570,40 @@ def camofox_navigate(url: str, task_id: Optional[str] = None) -> str:
return tool_error(str(e), success=False)
def _camofox_private_page_block(session: Dict[str, Any], task_id: Optional[str], action: str) -> Optional[str]:
"""Return a blocked payload when the current Camofox page is private/internal.
Mirrors the eval-path guard added for ``_camofox_eval`` (browser_tool.py):
Camofox snapshot / vision / image-extraction all read current page state, so
on a non-local backend they can leak the content of an intranet/metadata
page the terminal itself can't reach. The gate matches ``browser_snapshot``
/ ``browser_vision`` — only active when the SSRF guard applies (non-local
backend, not a local sidecar, ``allow_private_urls`` unset). Fail-open on
probe failure, matching the sibling guards.
Imports are deferred to call time because ``browser_tool`` imports this
module; importing it at module load would create a circular import.
"""
from tools.browser_tool import (
_camofox_current_page_private_url,
_eval_ssrf_guard_active,
)
if not _eval_ssrf_guard_active(task_id or "default"):
return None
blocked_url = _camofox_current_page_private_url(session["tab_id"], session["user_id"])
if not blocked_url:
return None
return json.dumps({
"success": False,
"error": (
"Blocked: page URL targets a private or internal address "
f"({blocked_url}). Refusing to {action} on this page in this "
"browser mode."
),
}, ensure_ascii=False)
def camofox_snapshot(full: bool = False, task_id: Optional[str] = None,
user_task: Optional[str] = None) -> str:
"""Get accessibility tree snapshot from Camofox."""
@@ -578,6 +612,10 @@ def camofox_snapshot(full: bool = False, task_id: Optional[str] = None,
if not session["tab_id"]:
return tool_error("No browser session. Call browser_navigate first.", success=False)
blocked = _camofox_private_page_block(session, task_id, "read a page snapshot")
if blocked:
return blocked
data = _get(
f"/tabs/{session['tab_id']}/snapshot",
params={"userId": session["user_id"]},
@@ -742,6 +780,10 @@ def camofox_get_images(task_id: Optional[str] = None) -> str:
if not session["tab_id"]:
return tool_error("No browser session. Call browser_navigate first.", success=False)
blocked = _camofox_private_page_block(session, task_id, "extract page images")
if blocked:
return blocked
import re
data = _get(
@@ -786,6 +828,10 @@ def camofox_vision(question: str, annotate: bool = False,
if not session["tab_id"]:
return tool_error("No browser session. Call browser_navigate first.", success=False)
blocked = _camofox_private_page_block(session, task_id, "capture a screenshot")
if blocked:
return blocked
# Get screenshot as binary PNG
resp = _get_raw(
f"/tabs/{session['tab_id']}/screenshot",