Skip to content

v1.13.0

Choose a tag to compare

@github-actions github-actions released this 03 Nov 13:07
· 174 commits to main since this release

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_TOKEN now requires a token it generates. On first start, a loopback controller
    with no configured credential writes a random token to API_TOKEN_FILE (/data/api-token, which is
    ./data/api-token in 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 set API_BEARER_TOKEN or API_BEARER_TOKENS keep using their token, and the file is
    ignored. Point each client at the token:
    • stdio MCP clients (Claude Desktop, Cursor): set AUTO_BROWSER_TOKEN_FILE to the absolute path of
      data/api-token in your checkout, and use this release's bridge (uvx auto-browser-mcp@latest). An older bridge
      ignores that setting, so set AUTO_BROWSER_BEARER_TOKEN to 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.md covers Codex, Cursor, VS Code and Antigravity.

    • The Python SDK and LangChain: pass token_file="data/api-token" to AutoBrowserClient, AutoBrowserTool,
      AutoBrowserTool.list_tools and AutoBrowserNode. 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 /metrics uses authorization: { type: Bearer, credentials_file: <path> }. The
      controller resets data/api-token to mode 0600 at every start, so a Prometheus running as another user needs its
      own readable copy, or set API_BEARER_TOKEN and give it that.

    • Pages a browser opens directly, such as GET /sessions/{id}/replay and /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 /replay page has no tokenless replacement.

    • Update your checkout as well as the images. This release's .gitignore covers data/api-token. On an older
      checkout, add data/api-token to .gitignore yourself, or git add -A can 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. Set API_TOKEN_FILE to a path
      under your user, or set API_BEARER_TOKEN. On Windows the default resolves to C:\data\api-token on 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_FILE yourself 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=true restores 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_ephemeral session 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 in DEFAULT_PROXY_SERVER or 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 use ISOLATED_BROWSER_NETWORK_MODE=shared.
    • ISOLATED_BROWSER_NETWORK now applies only with ISOLATED_BROWSER_NETWORK_MODE=shared. With per_session it
      is ignored, with a warning.
    • per_session needs the controller to run as a container on the isolation Docker daemon. A controller run on the
      host needs shared, and so does one that reaches another daemon through ISOLATED_DOCKER_HOST. A
      docker-socket-proxy needs NETWORKS=1 as well as CONTAINERS=1 and POST=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, widen default-address-pools in the
      daemon's daemon.json.
  • Each isolated session's noVNC has its own random password, and isolated containers no longer receive the
    operator's VNC_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_PASSWORD now guards only the shared browser-node. Set it in docker_ephemeral deployments; readiness warns
      when it is missing.
  • Browser containers keep only five Linux capabilities (CHOWN, DAC_OVERRIDE, KILL, SETGID, SETUID),
    with no-new-privileges; browser-node also gets pids_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's cap_add; isolated session
    containers always get these five, and no setting widens them.
  • /docs, /redoc and /openapi.json answer 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 global bearerAuth scheme, 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 /metrics or /artifacts now 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_TOKEN or API_BEARER_TOKENS entry is shorter
      than 32 characters.
  • docker_ephemeral sessions 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 answered GET /json with 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 through host.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_PASSWORD did 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 with no-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_FILE and API_ALLOW_TOKENLESS_LOOPBACK (above).
  • --token-file / AUTO_BROWSER_TOKEN_FILE for the stdio bridge, read on every request, and token_file for the
    Python SDK and LangChain.
  • ISOLATED_BROWSER_NETWORK_MODE (per_session or shared).

Fixed

  • Stealth sessions sent half a user agent. The USER_AGENT_POOL default was joined and split on commas, and every
    Chrome UA contains (KHTML, like Gecko). So each context with STEALTH_ENABLED=true got a fragment such as
    like Gecko) Chrome/149.0.0.0 Safari/537.36 as 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_POOL naming 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 (a fetch with credentials: "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-node and 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/list went 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_ephemeral on Docker Desktop for Windows: every isolated session failed to start ("too many colons"),
    because the controller turned the Windows path behind /data into 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.