Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions tests/tools/test_browser_private_page_action_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Regression tests for private-page browser interaction guards."""

import json

import pytest

from tools import browser_tool


PRIVATE_URL = "http://169.254.169.254/latest/meta-data/"


@pytest.fixture(autouse=True)
def _browser_mode(monkeypatch):
monkeypatch.setattr(browser_tool, "_is_camofox_mode", lambda: False)
monkeypatch.setattr(browser_tool, "_last_session_key", lambda task_id: task_id)


@pytest.mark.parametrize(
("tool_call", "args"),
[
(browser_tool.browser_click, ("@e1",)),
(browser_tool.browser_type, ("@e1", "do-not-send-this")),
(browser_tool.browser_press, ("Enter",)),
],
)
def test_private_page_blocks_state_changing_actions(monkeypatch, tool_call, args):
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)

def fail_run(*_args, **_kwargs):
raise AssertionError("browser command should not run on a private page")

monkeypatch.setattr(browser_tool, "_run_browser_command", fail_run)

out = json.loads(tool_call(*args, task_id="task-1"))

assert out["success"] is False
assert PRIVATE_URL in out["error"]
assert "private or internal address" in out["error"]
assert "do-not-send-this" not in json.dumps(out)


def test_click_still_runs_when_current_page_is_public(monkeypatch):
calls = []

monkeypatch.setattr(browser_tool, "_eval_ssrf_guard_active", lambda task_id: True)
monkeypatch.setattr(browser_tool, "_current_page_private_url", lambda task_id: None)

def fake_run(task_id, command, args):
calls.append((task_id, command, args))
return {"success": True}

monkeypatch.setattr(browser_tool, "_run_browser_command", fake_run)

out = json.loads(browser_tool.browser_click("e1", task_id="task-1"))

assert out == {"success": True, "clicked": "@e1"}
assert calls == [("task-1", "click", ["@e1"])]
26 changes: 26 additions & 0 deletions tools/browser_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2953,6 +2953,9 @@ def browser_click(ref: str, task_id: Optional[str] = None) -> str:
return camofox_click(ref, task_id)

effective_task_id = _last_session_key(task_id or "default")
blocked = _blocked_private_page_action(effective_task_id, "click")
if blocked is not None:
return blocked

# Ensure ref starts with @
if not ref.startswith("@"):
Expand Down Expand Up @@ -2991,6 +2994,9 @@ def browser_type(ref: str, text: str, task_id: Optional[str] = None) -> str:
return camofox_type(ref, text, task_id)

effective_task_id = _last_session_key(task_id or "default")
blocked = _blocked_private_page_action(effective_task_id, "type")
if blocked is not None:
return blocked

# Ensure ref starts with @
if not ref.startswith("@"):
Expand Down Expand Up @@ -3126,6 +3132,9 @@ def browser_press(key: str, task_id: Optional[str] = None) -> str:
return camofox_press(key, task_id)

effective_task_id = _last_session_key(task_id or "default")
blocked = _blocked_private_page_action(effective_task_id, "press")
if blocked is not None:
return blocked
result = _run_browser_command(effective_task_id, "press", [key])

if result.get("success"):
Expand All @@ -3145,6 +3154,23 @@ def browser_press(key: str, task_id: Optional[str] = None) -> str:



def _blocked_private_page_action(effective_task_id: str, action: str) -> Optional[str]:
"""Return a blocked payload when an unsafe cloud page would receive input."""
if not _eval_ssrf_guard_active(effective_task_id):
return None
blocked_url = _current_page_private_url(effective_task_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 browser_console(clear: bool = False, expression: Optional[str] = None, task_id: Optional[str] = None) -> str:
"""Get browser console messages and JavaScript errors, or evaluate JS in the page.

Expand Down
Loading