Repository navigation
v1.13.1
A security release over 1.13.0 (GHSA-q9w4-3w52-f9f2). A page on a host ALLOWED_HOSTS refuses could open a window that
reached the agent anyway, and with docker_ephemeral isolation the controller answered API requests from a session's
own Docker network. 1.13.1 judges every window when it is created, refuses requests from session networks, and runs
shadow browsing's Chromium with its sandbox on. Only the controller image changes: the browser-node image and the
client, LangChain and MCP bridge packages change only in version.
Upgrade notes
- With
ALLOWED_HOSTSrefusing any host, each browser gets a window-birth watcher (one browser-level CDP
session). A browser it cannot attach to gets no session: creating one answerswindow_birth_watcher_unavailable
(503, retryable), as the pre-request gate's own failure does, and/readinessreports the watcher. With
ALLOWED_HOSTSempty or*nothing changes. - A popup becomes readable one CDP round trip after it is reported.
activate_tabwaits for its verdict, at most
10 seconds, rather than refuse it. A popup a clean tab opens within 2 seconds of a refused tab closing is refused:
open it again after those 2 seconds. - Requests from a
docker_ephemeralsession network are refused (403). Nothing in the shipped setup calls the
controller from a session's browser container. - Shadow browsing launches its headed Chromium sandboxed, and a controller running as root refuses it. On Linux
the sandbox needs unprivileged user namespaces.
Security
-
A window that a refused page opened could reach the agent. The controller judged a popup by its opener
when Playwright reported it, and Playwright reports a window only once its first navigation commits. A window
whose first navigation committed nothing was reported after the agent's next navigate had cleared the opener,
so it was judged clean and read. The controller now judges every window when its CDP target is created, by its
opener, and closes a refused page's window that Playwright never reports once nothing justifies it. It fails
closed: a browser it cannot watch gets no session (window_birth_watcher_unavailable, 503, retryable), and
/readinessreports the watcher. WithALLOWED_HOSTSempty or*nothing changes and nothing attaches.
What changes for legitimate popups:- a reported popup becomes readable one CDP round trip later than before (
activate_tabwaits for it, at most
10 seconds), and what it logged meanwhile reaches the agent once it is judged clean, except for a popup that
closes before its target id is read (within milliseconds of loading, longer on a busy browser), which is
left unjudged and loses what it logged; - with the watcher attached and its reads succeeding, the one new refusal is a popup opened within 2 seconds of
a refused tab closing; - a window the controller cannot judge (its target id cannot be read, or it opened while the watcher was down)
is refused as unjudged, and the refusal says so and how to open it again; - each reported popup and open-tab tab keeps one page-level CDP session for its life: reading its target id
opens it, and detaching it lost the failed-request and download events of the windows that page opens.
The rarer fail-closed cases are listed in
docs/deployment.md. - a reported popup becomes readable one CDP round trip later than before (
-
The controller refuses requests from
docker_ephemeralsession networks. It joins each session's network to
reach that browser, so in 1.13.0 code that broke out of a session's browser could still reach the API at the
controller's address there, with only the token in its way. Browser containers never call the
controller, so it now answers 403 to any request whose direct peer is on a live session network, before the Host
check, the rate limiter and the token check. Forwarded-for headers are ignored. A session network that reports no
subnet is not used: the session fails to start rather than run unfenced. On Docker Desktop a session container can
still reach the API port published on127.0.0.1throughhost.docker.internal, where the controller sees the
host's own address; the Host allowlist and the token refuse it there. -
Shadow browsing runs its headed Chromium with Chromium's sandbox on. It launched with
--no-sandbox, and since
the shipped controller image has no browser for it, that meant host runs, where the sandbox works. A controller
running as root, where Chromium cannot start sandboxed, now refuses shadow browsing. When the sandboxed launch
fails (on Linux it needs unprivileged user namespaces), the error says so, and the controller log has Chromium's
own message. -
/docsand/redocload pinned Swagger UI (5.34.0) and ReDoc (2.5.4) with Subresource Integrity hashes, and
a Content-Security-Policy that admits nothing else from another origin (no Google Fonts, no third-party favicon,
no ReDoc logo from cdn.redoc.ly). Swagger's Authorize box takes the API token, and the pages loaded a floating
major version from a CDN with no integrity check. -
The smoke and doctor scripts no longer put the API token on curl's command line, where every local user could
read it in the process list. It reaches curl as a config on stdin, and stays out ofbash -xtraces.
Fixed
- A browser container that could not be removed no longer keeps its session network (and its address-pool slot)
until the next start: it is disconnected first. - A foreign Docker network holding a session network's name gives an error naming it and what to do, instead of
Docker's bare conflict. - The shadow-browse route logs why it refused (disabled, a root controller, no sandbox); callers still get a
generic 400.
Changed
- CI's compose-smoke runs with a
VNC_PASSWORDon the shared browser-node, so its VNC-authentication checks run.