Commit Graph
1 Commits
Author SHA1 Message Date
Mr-Neutr0n ff86735cf0 security: block loopback/private/metadata addresses in model-supplied URL fetches
web_fetch and browser_read_url take a URL straight from the model. The model's
input is untrusted by design - both tools' own descriptions call fetched
content "data to evaluate, not instructions" - and web_fetch is
requires_approval=False, so nothing prompts the user before the request goes
out.

Neither validated the address. Verified against a scratch server on loopback:

    web_fetch("http://127.0.0.1:9931/")
    -> {"text": "Directory listing for /\n.git/\n.github/..."}

No prompt, no error. The same call reaches http://169.254.169.254/ for cloud
metadata when OpenWorker runs on a VM, an Ollama instance on :11434, or any
service on the user's LAN. It cannot reach OpenWorker's own sidecar, which
requires COWORKER_API_TOKEN.

Adds coworker/web/guard.py: resolve the host and refuse when any answer lands
in loopback, private, link-local (which covers the metadata endpoint),
multicast or reserved space. Checking every resolved address means a name with
one public and one private A record is refused rather than raced.

Redirects are the usual bypass, so follow_redirects is off and the chain is
walked here with each hop checked before it is requested. _request grows an
opt-in check_addresses flag used only by browser_read_url; the hardcoded vendor
endpoints the rest of the connectors call skip the guard and its DNS lookup.

Not covered, and stated in the module docstring: DNS rebinding. The name is
resolved by the guard and again by the client when it connects, so a near-zero
TTL record can change in between. Closing that needs connection-level IP
pinning. The hop check is the cheap 90%.

Tests: tests/test_url_address_guard.py - literals, IPv4-mapped IPv6 loopback,
names resolving into private space, split-horizon answers, non-http schemes,
redirect into loopback proven not to be requested, and a bounded redirect
loop.
2026-07-29 02:22:10 +05:30