Repository navigation
v1.13.0
A security release over 1.12.4 (GHSA-4c52-f6vr-xrrf). A browser container that a page compromised could take over
other sessions' browsers and desktops and, on a controller without a token, the controller's API. 1.13.0 isolates each
docker_ephemeral session on its own network with its own noVNC password, makes a local controller generate and
require a token, and drops the browser containers' Linux capabilities. It changes a default that clients rely on:
read the upgrade notes first. Update your checkout (its compose files carry the container hardening), the
controller and browser-node images, and the client, LangChain and MCP bridge packages, together.
Upgrade notes
- A controller with no
API_BEARER_TOKENnow requires a token it generates. On first start, a loopback controller
with no configured credential writes a random token toAPI_TOKEN_FILE(/data/api-token, which is
./data/api-tokenin your checkout with the shipped compose, which pins that path). It then demands that token on
every API and MCP request; health checks,/version, the dashboard page and, on loopback, the API docs are exempt.
Deployments that already setAPI_BEARER_TOKENorAPI_BEARER_TOKENSkeep using their token, and the file is
ignored. Point each client at the token:-
stdio MCP clients (Claude Desktop, Cursor): set
AUTO_BROWSER_TOKEN_FILEto the absolute path of
data/api-tokenin your checkout, and use this release's bridge (uvx auto-browser-mcp@latest). An older bridge
ignores that setting, so setAUTO_BROWSER_BEARER_TOKENto the file's contents instead. The repo's own wrapper
(make stdio-bridge,scripts/mcp_stdio_bridge.py) finds the file itself for a loopback URL. -
HTTP MCP clients: send
Authorization: Bearer <token>. For Claude Code, run this from the auto-browser folder:claude mcp add --transport http --scope user auto-browser http://127.0.0.1:8000/mcp \ --header "Authorization: Bearer $(cat data/api-token)"docs/mcp-clients.mdcovers Codex, Cursor, VS Code and Antigravity. -
The Python SDK and LangChain: pass
token_file="data/api-token"toAutoBrowserClient,AutoBrowserTool,
AutoBrowserTool.list_toolsandAutoBrowserNode. A relative path resolves against the working directory. The
client reads the file when it is constructed; the LangChain tools and the bridge read it on every call. -
The dashboard asks for the token once per tab, or takes it in the URL as
/dashboard#token=<token>. -
Curl and scripts send the header.
-
A Prometheus scrape of
/metricsusesauthorization: { type: Bearer, credentials_file: <path> }. The
controller resetsdata/api-tokento mode 0600 at every start, so a Prometheus running as another user needs its
own readable copy, or setAPI_BEARER_TOKENand give it that. -
Pages a browser opens directly, such as
GET /sessions/{id}/replayand/artifacts/...links, cannot send
the header. They now need the token, as they already did on token deployments. For an agent run, the dashboard's
replay panel (which takes the job id) works; a session's/replaypage has no tokenless replacement. -
Update your checkout as well as the images. This release's
.gitignorecoversdata/api-token. On an older
checkout, adddata/api-tokento.gitignoreyourself, orgit add -Acan commit it. -
Host runs (not Docker): a loopback controller with no token that cannot write
API_TOKEN_FILE(default
/data/api-token, rarely writable outside a container) now refuses to start. SetAPI_TOKEN_FILEto a path
under your user, or setAPI_BEARER_TOKEN. On Windows the default resolves toC:\data\api-tokenon the
current drive, whose inherited permissions let other local users read and replace it, so set it there too. -
A token you place in
API_TOKEN_FILEyourself must be at least 32 printable characters, or the controller
refuses to start. It is used in every scope, so a mounted secret works for an exposed deployment too. -
API_ALLOW_TOKENLESS_LOOPBACK=truerestores the tokenless loopback API. It is ignored in production, and it
reopens the hole: any container on the compose network can drive that controller.
-
- Each
docker_ephemeralsession gets its own Docker network (ISOLATED_BROWSER_NETWORK_MODE=per_session, the
new default). The controller joins that network for the session's life.- An isolated browser no longer reaches compose-network services by name or container address: browser-node's
Playwright server, or a proxy sidecar named inDEFAULT_PROXY_SERVERor a proxy persona. Ports published on the
host stay reachable, browser-node's noVNC included, and so does the controller. Publish such a proxy another way,
or useISOLATED_BROWSER_NETWORK_MODE=shared. ISOLATED_BROWSER_NETWORKnow applies only withISOLATED_BROWSER_NETWORK_MODE=shared. Withper_sessionit
is ignored, with a warning.per_sessionneeds the controller to run as a container on the isolation Docker daemon. A controller run on the
host needsshared, and so does one that reaches another daemon throughISOLATED_DOCKER_HOST. A
docker-socket-proxy needsNETWORKS=1as well asCONTAINERS=1andPOST=1.- Session networks are named
<container>-net. At startup the controller removes leftover session networks along
with leftover containers, so run one isolation controller per Docker daemon. - Docker's default address pools allow about 30 bridge networks per daemon, shared with the default bridge, the
compose network and other projects. Above roughly 25 concurrent sessions, widendefault-address-poolsin the
daemon'sdaemon.json.
- An isolated browser no longer reaches compose-network services by name or container address: browser-node's
- Each isolated session's noVNC has its own random password, and isolated containers no longer receive the
operator'sVNC_PASSWORD.- The takeover URL in a session's own record carries the password in its URL fragment, so noVNC connects without
asking. That record is what creating or getting a session, remote access and the takeover request return, over
REST and MCP, so over MCP it reaches the calling model. Treat that URL as a secret. - The password is 8 characters, the most VNC authentication uses, and that authentication can be cracked offline by
anyone who watches the connection. Use the HTTPS tunnel modes when the takeover leaves the machine. - Lists, observations and stored records keep the password-free URL.
VNC_PASSWORDnow guards only the shared browser-node. Set it indocker_ephemeraldeployments; readiness warns
when it is missing.
- The takeover URL in a session's own record carries the password in its URL fragment, so noVNC connects without
- Browser containers keep only five Linux capabilities (
CHOWN,DAC_OVERRIDE,KILL,SETGID,SETUID),
withno-new-privileges; browser-node also getspids_limit: 2048, which counts threads. The controller runs with
no-new-privileges. With the isolation overlay, IP forwarding is also off in the controller. A custom image built
on browser-node that needs more capabilities has to add them to the compose service'scap_add; isolated session
containers always get these five, and no setting widens them. /docs,/redocand/openapi.jsonanswer without a token on a loopback deployment only, so a browser can
open them, with or without a configured token. Swagger's Authorize button takes the token, and the OpenAPI schema
now declares a globalbearerAuthscheme, which changes generated clients. An exposed deployment still requires
the token there.
Security
- A local controller generates and requires its own token.
- Before, a controller with no
API_BEARER_TOKEN(the default compose setup) answered unauthenticated requests
whose Host header named a loopback host. Native code in any container on the compose network can send that
header, and it reached stored-login export, approvals and agent runs on the operator's provider keys. - A wrong-token guess at
/metricsor/artifactsnow counts against the per-IP rate limit. - The escape hatch never applies in production.
- The controller logs a startup warning when a loopback
API_BEARER_TOKENorAPI_BEARER_TOKENSentry is shorter
than 32 characters.
- Before, a controller with no
docker_ephemeralsessions no longer share a network. Every session's browser container used to join the
controller's network, as did browser-node. Each one's Playwright server answeredGET /jsonwith its WebSocket
path and checked neither Host nor Origin. Code in one session's container could therefore drive every other
session's browser.- Each isolated session's noVNC has its own password. A published noVNC port was reachable from other sessions'
containers: by container address, and on Docker Desktop throughhost.docker.internal. Native code can forge the
Host and Origin headers noVNC checks, so without a password the session's desktop was open to it. A shared
VNC_PASSWORDdid not help, because each container held a copy its browser user could read. - Browser containers drop every Linux capability except the five their entrypoint uses. Browser-node, the isolated
browser containers and the controller run withno-new-privileges. Chromium runs without its own sandbox in these
containers, so this narrows what a renderer exploit gets. - The isolation controller no longer forwards IP traffic between the session networks it joins
(net.ipv4.ip_forward=0). - The stdio bridge never follows a redirect. Python's urllib re-sent a request answered 301, 302 or 303 as a GET
to the redirect's target with the same headers, so a redirecting endpoint received the bridge's token, on any host.
MCP never redirects, and the bridge now reports a redirect as an HTTP error.
Added
API_TOKEN_FILEandAPI_ALLOW_TOKENLESS_LOOPBACK(above).--token-file/AUTO_BROWSER_TOKEN_FILEfor the stdio bridge, read on every request, andtoken_filefor the
Python SDK and LangChain.ISOLATED_BROWSER_NETWORK_MODE(per_sessionorshared).
Fixed
- Stealth sessions sent half a user agent. The
USER_AGENT_POOLdefault was joined and split on commas, and every
Chrome UA contains(KHTML, like Gecko). So each context withSTEALTH_ENABLED=truegot a fragment such as
like Gecko) Chrome/149.0.0.0 Safari/537.36as its user agent, which is a stronger bot signal than the default.
Entries are now separated by|or newlines. A value with neither is one user agent, commas included, so a
USER_AGENT_POOLnaming a single UA now works too. - The stealth UA pool claims Chrome 156 and 155, and Edge 156, to match the Chromium 156 that 1.12.4's
browser-node ships. It claimed Chrome 148 and 149, while the browser's Client Hints (Sec-CH-UA,
navigator.userAgentData) report Chromium 156. - Attach modes on Chrome 155 and later: the documented switch names a second feature. While the pre-request
gate is attached (CDP_CONNECT_URL,browser.cdp_attach), Chrome 155+ aborts a credentialed cross-origin request
that needed a CORS preflight once it is redirected (afetchwithcredentials: "include"and a custom header,
answered 302 to another origin), in every tab, the operator's own included (Playwright #43242). Start that Chrome
with--disable-features=BlockOriginHeaderModificationOnRedirect,AvoidCorsURLLoaderRestartOnRedirect. The
readiness check, the gate's explanation and the docs name both.browser-nodeand every browser the controller
launches are not affected: Playwright 1.64 disables the feature there. - The stdio bridge on Windows spoke the console's code page, not UTF-8. A client that launches it with pipes
(Claude Desktop, Cursor) handed it cp1252.tools/listwent out as invalid UTF-8, and observing a page with text
outside cp1252 crashed the bridge and left its browser session open. It now reads and writes UTF-8 on every
platform. docker_ephemeralon Docker Desktop for Windows: every isolated session failed to start ("too many colons"),
because the controller turned the Windows path behind/datainto a Linux one. It now passes the Windows path
through.- The dashboard's replay screenshots load again on a controller with a token. They were bare image links, which
cannot send a token. The session list's noVNC link now opens the session's own record, which carries its noVNC
password.