Commit Graph

1573 Commits

Author SHA1 Message Date
Magnus Müller c7da2c0e10 Fix Rust PR review feedback 2026-06-06 04:59:30 +00:00
Magnus Müller 1474c60da1 Merge remote-tracking branch 'upstream/main' into magnus/browser-use-rust-core-integration
# Conflicts:
#	browser_use/__init__.py
#	browser_use/agent/service.py
#	browser_use/browser/watchdogs/downloads_watchdog.py
#	browser_use/code_use/utils.py
#	browser_use/llm/models.py
#	browser_use/tokens/service.py
#	browser_use/utils.py
2026-06-04 16:14:57 +00:00
Magnus Müller 20e70519cf Filter generic network auto-downloads 2026-06-04 09:10:02 +00:00
Saurav Panda f0413fbd8c refactor(downloads): tighten download-sanitization comments
Drop the per-call-site rationale text; the helper names already convey
what's happening, and the WHY lives in the commit history. Shrink both
helper docstrings to one line.

No behavior change; 16/16 tests still pass.
2026-05-18 17:31:09 -07:00
Saurav Panda 29b03e43b4 Merge branch 'main' into fix/sec-download-path-traversal 2026-05-18 17:28:52 -07:00
Saurav Panda 4866656bba refactor(security): tighten _is_ip_address comments
Condense the running narrative across four prior fix commits into a single
docstring summarizing what canonicalization the function mirrors and the
never-throw invariant. Drop the per-step justifications — those live in
the commit messages where they belong.

No behavior change; 40/40 tests still pass.
2026-05-18 16:46:55 -07:00
Saurav Panda 7d061a0327 fix(security): fold IDNA label separators before IPv4 classification
Addresses third P1 codex review on #4866.

Per RFC 3490 / UTS46, four code points act as label separators in IDNA
processing — `.` (U+002E), `。` (U+3002 IDEOGRAPHIC FULL STOP),
`.` (U+FF0E FULLWIDTH FULL STOP), and `。` (U+FF61 HALFWIDTH IDEOGRAPHIC
FULL STOP). WHATWG URL parsing folds all four to `.` before resolution,
so `http://127。0。0。1/` and `http://127。0。0。1/` reach 127.0.0.1.

NFKC handles only U+FF0E and partially maps U+FF61 → U+3002, leaving the
IPv4 parser unable to match the dominant ideographic-dot form. Explicitly
replace U+3002 and U+FF61 with `.` after NFKC.

Test `test_idna_dot_separators_blocked` covers all four dot variants
both at `_is_url_allowed` and `_is_ip_address` levels, plus a combined
case with circled digits (`①②⑦。⓪。⓪。①`).
2026-05-18 16:40:24 -07:00
Saurav Panda e34d2cc6c2 fix(security): NFKC-normalize host before IPv4 classification
Addresses second P1 codex review on #4866.

WHATWG URL canonicalization maps fullwidth digits (`127.0.0.1`),
circled digits (`①②⑦.⓪.⓪.①`), and Unicode-prefixed hex forms
(`0x7f000001`) to ASCII IPv4 literals (`127.0.0.1` and `0x7f000001`)
before resolution. The classifier saw only the original string and
returned False, so `block_ip_addresses=True` was still bypassable by
re-encoding the IP with any equivalent Unicode digit variant.

Add a `unicodedata.normalize('NFKC', ...)` step after percent-decoding
and before parsing. NFKC handles all the bot's example forms and is
stdlib-only (no IDNA dependency). Wrapped in try/except to preserve the
never-throw invariant.

Tests:
- test_unicode_normalized_ipv4_blocked covers fullwidth, fullwidth+ASCII
  hex, and circled-digit forms at both `_is_url_allowed` and
  `_is_ip_address` boundaries.
- test_idn_domains_not_misclassified_as_ip is a false-positive guard
  ensuring legitimate IDN domains (`café.example`, `日本.example`, their
  punycode equivalents) remain classified as domains.
2026-05-18 16:26:51 -07:00
Saurav Panda b6ef0e2888 fix(security): percent-decode host before IPv4 classification
Addresses second codex review on #4866 (P1).

Chromium percent-decodes the host component before applying its IPv4
parser, so a URL like `http://%30x7f000001/` (decodes to `0x7f000001`,
i.e. 127.0.0.1) or `http://%31%32%37.0.0.1/` (decodes to `127.0.0.1`)
still reaches the IP address despite `block_ip_addresses=True`. Without
decoding, `_is_ip_address` sees the literal `%`-encoded string and
returns False — bypassing the block.

Call `urllib.parse.unquote` on the host before passing it to both
`ipaddress.ip_address` and `socket.inet_aton`. Wrap in try/except to
preserve the never-throw invariant for the classifier.

Adds two regression tests:
- test_percent_encoded_ipv4_blocked covers mixed (`%30x7f000001`), fully
  encoded canonical (`%31%32%37.0.0.1`), and fully encoded decimal
  (`%32%31%33%30%37%30%36%34%33%33`) bypass forms.
- test_malformed_percent_encoding_does_not_crash covers lone `%`, `%zz`,
  `%2` — `unquote` leaves these as-is and the classifier must not throw.
2026-05-18 16:17:26 -07:00
Saurav Panda b0d543fff4 fix(security): catch non-OSError failures from inet_aton
Addresses codex review on #4866.

`socket.inet_aton` raises `UnicodeEncodeError` (not `OSError`) for
hostnames containing lone surrogates — common in URLs produced by
URL-decoding malformed UTF-8. The earlier patch only caught `OSError`,
so `_is_ip_address('\udcff')` would propagate the exception through
`_is_url_allowed` and crash the navigation security check whenever
`block_ip_addresses=True`.

Restore the original code's defensive `except Exception` posture for
both `ipaddress.ip_address` and `socket.inet_aton`. The classifier
should never throw — it returns True (recognized IP) or False (not a
recognizable IP); downstream domain-allowlist handling then applies.

Regression test covers lone-surrogate hostnames in
`test_malformed_unicode_hostnames_do_not_crash_classifier`.
2026-05-18 15:52:46 -07:00
Saurav Panda c2ac67ab90 fix(downloads): sanitize attacker-controlled filenames and verify containment
GHSA-rv9j-wqjp-2fv4 (critical), GHSA-66xh-g88g-2h8j, GHSA-hpr4-fqgr-xhj9.

`DownloadsWatchdog` joined attacker-controlled filenames from CDP
(`Page.downloadWillBegin.suggestedFilename`) and `Content-Disposition`
headers directly into the configured `downloads_path`. Strings like
`../../escape.bin` or `/etc/shadow.bak` would `os.path.join` outside the
downloads directory, writing the fetched bytes (also attacker-controlled
— the response body is the exploit content) to an arbitrary location
with the agent's process privileges.

`download_file_from_url` triggers passively for any
`Content-Disposition: attachment` response, so this is reachable from any
visited site — `allowed_domains` does not mitigate it.

Add two private helpers on DownloadsWatchdog:

- `_sanitize_download_filename(name)`: keep only the basename, normalize
  Windows separators, strip null bytes, fall back to `'download'` for
  empty / pure-traversal inputs.
- `_is_path_contained(path, dir)`: realpath containment check for the
  on-disk sinks.

Wire the sanitizer at every attacker-controlled filename ingress:

- `download_will_begin_handler` (CDP suggestedFilename → cache + events)
- `_handle_cdp_download` (same field, separate path)
- Network-monitor Content-Disposition parser
- `download_file_from_url` (suggested_filename argument)
- `_handle_download` (Playwright `download.suggested_filename`)

Wire the containment check at every on-disk write site:

- `download_file_from_url` write
- `_handle_download` (Playwright save_as path)
- `trigger_pdf_download` write (defense in depth — already basename'd)
2026-05-18 13:52:24 -07:00
Saurav Panda 626bda9072 fix(security): canonicalize non-standard IPv4 forms in block_ip_addresses
GHSA-xrfv-gg9f-wwjp, GHSA-g27c-8gp4-28cv.

`SecurityWatchdog._is_ip_address` only recognized IP strings that
`ipaddress.ip_address()` accepts — i.e. the canonical dotted-quad form
(`127.0.0.1`) and full IPv6. Chromium and the kernel resolver, however,
also accept several non-standard IPv4 representations:

  http://2130706433/     → 127.0.0.1   (decimal int)
  http://0x7f000001/     → 127.0.0.1   (hex)
  http://0177.0.0.1/     → 127.0.0.1   (octal)
  http://127.1/          → 127.0.0.1   (short-form)
  http://127.0.1/        → 127.0.0.1   (short-form)

`block_ip_addresses=True` was therefore trivially bypassed by re-encoding
the IP in any of these forms.

Fall back to `socket.inet_aton` after `ipaddress.ip_address()` fails — it
accepts the same liberal IPv4 forms the kernel resolver does, so the
classifier matches the browser's behavior.

The existing `test_ipv4_lookalike_domains_allowed` test was codifying the
buggy behavior for `1.2.3` (which IS a short-form IPv4 == 1.2.0.3).
Removed that assertion and added a dedicated `TestNonStandardIPv4Representations`
class covering decimal/hex/octal/short-form blocking, lookalike-domain
non-interference, and the interaction with `allowed_domains`.
2026-05-18 13:47:17 -07:00
LarsenCundric 9a6510a85c fix: respect proxy_country_code=None to disable proxy in cloud mode
When a user passes proxy_country_code=None to BrowserSession, they
intend to disable the proxy. Previously, None was indistinguishable
from "not passed" because both used None as the default value. The
server then applied its default (US proxy) instead of disabling it.

Introduces an _UNSET sentinel to distinguish "not passed" (server
default) from "explicitly None" (no proxy). When proxy_country_code
is explicitly set to None, it is now included in the CreateBrowserRequest
and sent as null in the API body, which the server correctly interprets
as "disable proxy".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-18 11:23:41 -07:00
Larsen Cundric 02dd650a19 fix: handle multi-character upper/lower in CDP typing
ord() raises TypeError when called on a multi-character string. Several
Unicode characters expand on case conversion ('ß'.upper() == 'SS',
'ffi'.upper() == 'FFI', 'ʼn'.upper() == 'ʼN'). Typing any of these caused
'Failed to input text via CDP: TypeError: ord() expected a character,
but string of length 2 found' and aborted the action.

Fall back to the original character's code point when upper()/lower()
returns more than one code point. The Windows VK code is only an
approximation anyway; the 'text' field passed to dispatchKeyEvent
carries the actual character.
2026-05-11 08:03:49 +00:00
duyua9 e89f3aaae2 fix: handle locked Chrome profile copies 2026-05-09 23:17:17 +08:00
Larsen Cundric 49fb16b693 fix(BrowserError): drop trailing 'during: None' when no event is attached
BrowserError.__str__ unconditionally appended 'during: <while_handling_event>',
so any error with details set but no event (e.g. ScrollToText 'Text not found',
click failures raised from action handlers) renders as

    Text not found: "X" ({'text': 'X'}) during: None

The 'during: None' tail is logging noise that surfaces ~1.6k times/day in
prod (Datadog: status:error env:production '"\'\}) during: None"', 24h).

Build the string from the parts that are actually populated, instead of
two mutually-exclusive branches that always print one of them.
2026-05-05 16:04:59 +00:00
Saurav Panda 98a86bead7 Merge branch 'main' into main 2026-05-01 15:43:05 -07:00
Saurav Panda 847ad78365 address cubic review: validate constructor timeout + exercise real send_raw
Two P2 comments from cubic on 9a09c4d7:

1. TimeoutWrappedCDPClient.__init__ trusted its cdp_request_timeout_s arg
   blindly. nan / inf / <=0 would either make every CDP call time out
   immediately (nan) or disable the guard (inf / <=0) — same defensive
   gap we already fixed for the env-var path. Extracted _coerce_valid_
   timeout() that mirrors _parse_env_cdp_timeout's validation; constructor
   now routes through it, so both entry points are equally safe.

2. test_send_raw_times_out_on_silent_server used an inline copy of the
   wrapper logic rather than the real TimeoutWrappedCDPClient.send_raw.
   A regression in the production method — e.g. accidentally removing
   the asyncio.wait_for — would not fail the test. Rewrote to:
   - Construct via __new__ (skip CDPClient.__init__'s WebSocket setup)
   - unittest.mock.patch the parent CDPClient.send_raw with a hanging
     coroutine
   - Call the real TimeoutWrappedCDPClient.send_raw, which does
     super().send_raw(...) → our patched stub
   - Assert it raises TimeoutError within the cap

Also added test_send_raw_passes_through_when_fast (fast-path regression
guard) and test_constructor_rejects_invalid_timeout (validation for
fix #1). All 14 tests in the timeout suite pass locally.
2026-04-20 17:54:37 -07:00
Saurav Panda 9a09c4d7dc fix(cdp): timeout-wrap CDPClient.send_raw to break silent WebSocket hangs
cdp_use.CDPClient.send_raw awaits a future that only resolves when the
browser sends a response with a matching message id. There is no timeout
on that await. Against the cloud browser service, the failure mode we
observed is: WebSocket stays alive at the TCP/keepalive layer (proxy
keeps pong-ing our pings), but the browser upstream is dead / unhealthy
and never sends any CDP response. send_raw's future never resolves, and
every higher-level timeout in browser-use (session.start's 15s connect
guard, agent.step_timeout, tools.act's action timeout) relies on
eventually getting a response — so they all wait forever too.

Evidence from a 170k-task collector run: 1,090 empty-history traces,
100% hit the 240s outer watchdog, median duration 582s, max 2214s, with
cloud HTTP layer clean throughout (all 200/201). One sample showed
/json/version returning 200 OK and then 5 minutes of total silence on
the WebSocket before forced stop — classic silent-hang.

Fix: add TimeoutWrappedCDPClient, a thin subclass of cdp_use.CDPClient
that wraps send_raw in asyncio.wait_for(timeout=cdp_request_timeout_s).
Any CDP method that doesn't respond within the cap raises plain
TimeoutError, which propagates through existing `except TimeoutError`
handlers in session.py / tools/service.py. Uses the same defensive env
parse pattern as BROWSER_USE_ACTION_TIMEOUT_S — rejects empty /
non-numeric / nan / inf / non-positive values with a warning fallback.

Default is 60s: generous for slow operations like Page.captureScreenshot
or Page.printToPDF on heavy pages, but well below the 180s step timeout
and any typical outer watchdog. Override via BROWSER_USE_CDP_TIMEOUT_S.

Wired into both CDPClient construction sites in session.py (initial
connect + reconnect path). All 17 existing real-browser tests
(test_action_blank_page, test_multi_act_guards) still pass.
2026-04-20 17:40:32 -07:00
Saurav Panda 132756dabb Address PR review feedback for record start/stop
- `on_BrowserConnectedEvent` now catches `RuntimeError` from
  `start_recording()` so sessions with `record_video_dir` configured but
  missing `[video]` extras (or a viewport that can't be sized) keep
  starting — prior graceful-degradation behavior is restored.
- Lazy `RecordingWatchdog` in the CLI handler now calls
  `attach_to_session()`, so `AgentFocusChangedEvent` / `BrowserStopEvent`
  handlers are wired correctly if the session dispatches them.
- Daemon shutdown finalizes any in-progress recording before tearing the
  browser down, preventing truncated MP4s on `close`, idle timeout, or
  signal-driven exit.
- Added regression test that monkeypatches `start_recording` to raise and
  asserts `on_BrowserConnectedEvent` swallows it without breaking startup.
2026-04-20 15:11:49 -07:00
Saurav Panda b1d933258c Add record start/stop CLI command for session video capture
Closes #4533.

- `RecordingWatchdog` gains public `start_recording(path, size?, framerate?)`,
  `stop_recording() -> Path`, and `is_recording`; the existing
  `BrowserConnectedEvent`/`BrowserStopEvent` path is refactored to use them,
  so profile-driven recording behavior is unchanged.
- `browser-use record start <path>` / `record stop` / `record status`
  subcommands wired through argparse, daemon dispatch, and the browser
  command handler. `record stop` prints the saved file path so it can be
  captured programmatically, matching the issue's requested UX. Works with
  `--session NAME` via the existing named-daemon infrastructure.
- The CLI's `CLIBrowserSession` intentionally skips watchdogs; the handler
  lazily instantiates `RecordingWatchdog` on first `record start` so CLI
  recording doesn't pay the watchdog-setup cost for non-recording sessions.
- Output format is `.mp4` (libx264) since that's what the existing
  `VideoRecorderService` encodes; optional dependency gate is unchanged
  (`pip install "browser-use[video]"`).
- New `tests/ci/test_action_record.py` exercises the full stack against a
  real headless browser + `pytest-httpserver`, verifying decodable MP4
  output, double-start rejection, stop-without-start no-op, that the
  existing `profile.record_video_dir` flow still works, and the argparse /
  dispatch wiring.
2026-04-20 14:50:52 -07:00
Laith Weinberger 4476f6e16e fix input clear fallbacks and clarify clear-then-type behavior 2026-04-15 17:31:04 -04:00
Alisha Ukani 628d8f44a7 Search for playwright-installed Chromium in chrome-mac or chrome-mac-arm64 folders 2026-04-14 13:10:31 -07:00
laithrw 76604913ad Merge branch 'main' into fix/browser-session-close 2026-04-12 17:45:44 -04:00
Laith Weinberger c1eb87a35f close alias for BrowserSession stop
thousands of users have attempted to use close, so why not add it
2026-04-12 17:43:04 -04:00
Laith Weinberger 03e2bc4da8 prefer Playwright chromium over sys Chrome by default 2026-04-12 11:41:50 -04:00
laithrw 60e7767228 Merge branch 'main' into issue-4631-review 2026-04-11 18:07:12 -04:00
Laith Weinberger 534eaafe7a clear dom cache after scroll to prevent stale extract data 2026-04-11 18:03:06 -04:00
Alezander9 76569995fd Improve OSS-to-cloud conversion: UTM tracking, better error messages, and cloud nudges
- Add UTM params to all cloud-bound links across README, CLI, and error messages
- Rewrite README Open Source vs Cloud section: position cloud browsers as
  recommended pairing for OSS users, remove separate Use Both section
- Rewrite error messages for use_cloud=True and ChatBrowserUse() to clearly
  state what is wrong and what to do next
- Add missing URLs: invalid API key now links to key page, insufficient
  credits now links to billing page
- Add cloud browser nudge on captcha detection (logger.warning)
- Add cloud browser nudge on local browser launch failure
2026-04-08 22:05:50 -07:00
shawn pana a7b476ee46 Merge branch 'main' into multi-session 2026-04-02 13:02:51 -07:00
ShawnPana f27c567aad fix: enable_recording defaults False in library, configurable in CLI
Library keeps recording off by default. CLI reads cloud_connect_recording
from config (defaults True). Users can disable with:
  browser-use config set cloud_connect_recording false
2026-04-01 22:22:33 -07:00
MagMueller 3aa68384ad style: use TimeoutError instead of asyncio.TimeoutError (py3.11+) 2026-04-01 21:54:58 -07:00
MagMueller dcc7e691c4 fix: add 2s timeout to pending network request check in DOM watchdog
On slow CI machines, _get_pending_network_requests() can hang for 15s+
when Chrome is busy loading/rendering after a navigation. This silently
eats into the 30s BrowserStateRequestEvent budget, leaving insufficient
time for the actual DOM capture — causing 5 consecutive timeouts and
agent termination.

Observed on eBay search results in eval runs: DOMWatchdog started but
produced zero log output for 15 seconds before the timeout killed it.
The pending network check was the first await after the URL log.
2026-04-01 21:40:06 -07:00
shawn pana f9f56b8392 Merge branch 'main' into multi-session 2026-04-01 21:31:42 -07:00
ShawnPana 032e73eec7 feat: config-driven proxy/timeout for cloud connect, enable recording
cloud connect reads cloud_connect_proxy and cloud_connect_timeout from
~/.browser-use/config.json. Recording always enabled via enableRecording
default on CreateBrowserRequest. No CLI flags — edit config for custom
settings, use cloud v2 REST for full control.
2026-03-31 13:44:27 -07:00
LarsenCundric 5e644981e8 fix: close CloudBrowserClient httpx pool on session stop to prevent memory leak
BrowserSession.on_BrowserStopEvent calls stop_browser() but never calls
_cloud_browser_client.close(), leaving the httpx connection pool alive.
On Lambda provisioned concurrency, these pools accumulate across
invocations — memory climbs from ~1.3GB to the 3GB ceiling over hours,
triggering OOM kills (21 Runtime.ExitError crashes in 6 hours observed
in production).

Changes:
- Call _cloud_browser_client.close() in on_BrowserStopEvent after
  stop_browser completes (in a finally block so it runs even if
  stop_browser fails)
- Make CloudBrowserClient.close() idempotent (check is_closed before
  calling aclose) so it's safe to call multiple times

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:19:43 -07:00
laithrw 4c883feb7c Merge branch 'main' into fix/cloud-session-stop-cdp-reconnect 2026-03-25 19:34:25 -04:00
laithrw a2313d9665 Merge branch 'main' into feature/cdp-navigation-timeout 2026-03-25 17:30:42 -04:00
Laith Weinberger 393d2c5b0f rm pass 2026-03-25 16:56:36 -04:00
Laith Weinberger e44ce7ce73 fix linux profile detection for chromium users 2026-03-24 18:14:22 -04:00
revanthkumar96 1c4182d4ec Add nav_timeout parameter to control CDP Page.navigate() timeout
- Add nav_timeout parameter to navigate_to_url_and_wait() method
- Pass nav_timeout from NavigateToUrlEvent to control CDP navigation timeout
- Fix issue where TIMEOUT_NavigateToUrlEvent env var only controlled outer timeout but CDP call had hardcoded 20s timeout
- Add comprehensive test case to verify env var controls CDP timeout
2026-03-24 11:19:34 +05:30
Laith Weinberger 29da480659 replace MV2 extensions with MV3 + remove chrome flags that block extension loading 2026-03-22 14:17:56 -04:00
Saurav Panda 4a65aea6a9 Merge branch 'main' into auto-switch-new-tab 2026-03-21 17:17:32 -07:00
Saurav Panda 03de83a069 Merge branch 'main' into iframe-click-issue 2026-03-21 15:57:02 -07:00
Saurav Panda 3b8f2457b1 fix: index elements inside cross-origin iframes reliably 2026-03-21 15:54:35 -07:00
Saurav Panda e7a05cbe86 fix: auto-switch to new tabs after click and add checkbox toggle fallback
- AGI-569: after any click that opens a new tab, automatically dispatch
  SwitchTabEvent so the agent lands on the new page immediately instead
  of requiring a manual switch step (~877 occurrences)

- AGI-548: for <input type="checkbox/radio">, capture checked state
  before the CDP mouse click and verify it toggled afterward; if
  unchanged (custom-styled or shadow-DOM-backed inputs), fall back to
  JS element.click() and report the final checked state in metadata
  (~1,241 occurrences)
2026-03-20 12:43:58 -07:00
shawn pana a2c2fa33aa Merge branch 'main' into cli 2026-03-20 09:34:45 -07:00
reformedot 2675c52e65 Enhance User-Agent header handling in BrowserSession
- Updated the BrowserSession class to ensure the User-Agent header is set correctly for both local and remote connections.
- Introduced a utility function to retrieve the browser-use version and incorporated it into the User-Agent string.
- Ensured headers are consistently handled as dictionaries to avoid potential issues with None values.

This change improves the reliability of the CDP client communication by providing a proper User-Agent, which can be crucial for certain web interactions.
2026-03-19 23:33:10 -07:00
ShawnPana 91c02012c3 merge origin/main into cli, resolve agent.py delete conflict 2026-03-19 21:06:38 -07:00
ShawnPana 694a111fad add upload command to CLI, extract find_file_input_near_element to BrowserSession
- Add `browser-use upload <index> <path>` command for uploading files to
  file input elements via the CLI
- Extract find_file_input_near_element from nested closures in tools/service.py
  to a reusable method on BrowserSession, deduplicating two copies
- Add BrowserWrapper.upload() for the Python REPL
- Resolve file paths to absolute on the client side before sending to daemon
- Update SKILL.md files and README with upload command docs
2026-03-19 17:02:34 -07:00