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:
commit
60b1f6ce3f
2 changed files with 116 additions and 0 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue