Merge pull request #56526 from srojk34/fix/browser-back-private-network-guard

security(browser): re-check private-network guard after browser_back navigation
This commit is contained in:
kshitij 2026-07-02 00:15:16 +05:30 committed by GitHub
commit 60b1f6ce3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 116 additions and 0 deletions

View file

@ -104,3 +104,100 @@ def test_camofox_short_circuits_before_guard(monkeypatch):
out = json.loads(browser_tool.browser_click("@e1", task_id="task-1"))
assert out == {"success": True, "camofox": True}
# ---------------------------------------------------------------------------
# browser_back — unlike click/type/press (check current page BEFORE acting),
# going back IS the navigation: the guard must fire AFTER _run_browser_command
# reports success, checking the page it just landed on, not the page it left.
# ---------------------------------------------------------------------------
def test_browser_back_blocks_when_landed_page_is_private(monkeypatch):
"""Browser history can land on a private/internal address the initial
browser_navigate preflight never saw the same class of gap already
closed for browser_snapshot/vision/console/eval and click/type/press."""
monkeypatch.setattr(browser_tool, "_eval_ssrf_guard_active", lambda task_id: True)
monkeypatch.setattr(browser_tool, "_current_page_private_url", lambda task_id: PRIVATE_URL)
monkeypatch.setattr(
browser_tool, "_run_browser_command",
lambda task_id, command, args: {"success": True, "data": {"url": PRIVATE_URL}},
)
out = json.loads(browser_tool.browser_back(task_id="task-1"))
assert out["success"] is False
assert PRIVATE_URL in out["error"]
assert "private or internal address" in out["error"]
# The blocked payload must not itself leak the raw URL as a "url" field
# the way the success payload does.
assert "url" not in out
def test_browser_back_returns_url_when_landed_page_is_public(monkeypatch):
monkeypatch.setattr(browser_tool, "_eval_ssrf_guard_active", lambda task_id: True)
monkeypatch.setattr(browser_tool, "_current_page_private_url", lambda task_id: None)
monkeypatch.setattr(
browser_tool, "_run_browser_command",
lambda task_id, command, args: {"success": True, "data": {"url": "https://example.com/"}},
)
out = json.loads(browser_tool.browser_back(task_id="task-1"))
assert out == {"success": True, "url": "https://example.com/"}
def test_browser_back_guard_inactive_does_not_probe(monkeypatch):
"""When the SSRF guard is inactive (local backend), back navigation must
proceed without even probing the landed page URL."""
monkeypatch.setattr(browser_tool, "_eval_ssrf_guard_active", lambda task_id: False)
def fail_probe(task_id):
raise AssertionError("_current_page_private_url must not be probed when guard inactive")
monkeypatch.setattr(browser_tool, "_current_page_private_url", fail_probe)
monkeypatch.setattr(
browser_tool, "_run_browser_command",
lambda task_id, command, args: {"success": True, "data": {"url": "https://example.com/"}},
)
out = json.loads(browser_tool.browser_back(task_id="task-1"))
assert out == {"success": True, "url": "https://example.com/"}
def test_browser_back_failed_navigation_does_not_probe(monkeypatch):
"""No page change happened, so there is nothing new to check — the guard
must not fire (or probe) on a failed back navigation."""
monkeypatch.setattr(browser_tool, "_eval_ssrf_guard_active", lambda task_id: True)
def fail_probe(task_id):
raise AssertionError("must not probe when the back navigation itself failed")
monkeypatch.setattr(browser_tool, "_current_page_private_url", fail_probe)
monkeypatch.setattr(
browser_tool, "_run_browser_command",
lambda task_id, command, args: {"success": False, "error": "no history"},
)
out = json.loads(browser_tool.browser_back(task_id="task-1"))
assert out == {"success": False, "error": "no history"}
def test_browser_back_camofox_short_circuits_before_guard(monkeypatch):
monkeypatch.setattr(browser_tool, "_is_camofox_mode", lambda: True)
def fail_guard(task_id):
raise AssertionError("guard must not run in camofox mode")
monkeypatch.setattr(browser_tool, "_eval_ssrf_guard_active", fail_guard)
monkeypatch.setattr(browser_tool, "_current_page_private_url", fail_guard)
import tools.browser_camofox as camofox
monkeypatch.setattr(camofox, "camofox_back", lambda task_id: '{"success": true, "camofox": true}')
out = json.loads(browser_tool.browser_back(task_id="task-1"))
assert out == {"success": True, "camofox": True}

View file

@ -3182,6 +3182,25 @@ def browser_back(task_id: Optional[str] = None) -> str:
result = _run_browser_command(effective_task_id, "back", [])
if result.get("success"):
# Browser history can land on a private/internal/cloud-metadata
# address that the browser_navigate preflight never saw (e.g. a
# redirect chain from an earlier legitimate navigation touched an
# internal host, or client-side history was otherwise manipulated).
# Re-check post-navigation, matching every other content-returning
# entry point (browser_snapshot/vision/console/eval, and click/type/
# press via _blocked_private_page_action) — the floor must fire for
# every backend, not just the initial navigate.
if _eval_ssrf_guard_active(effective_task_id):
_blocked_url = _current_page_private_url(effective_task_id)
if _blocked_url:
return json.dumps({
"success": False,
"error": (
"Blocked: page URL targets a private or internal address "
f"({_blocked_url}). Browser history navigation (back) "
"landed on this address."
),
}, ensure_ascii=False)
data = result.get("data", {})
response = {
"success": True,