Skip to content

v1.13.1

Choose a tag to compare

@github-actions github-actions released this 04 Nov 09:17
· 96 commits to main since this release

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_HOSTS refusing 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 answers window_birth_watcher_unavailable
    (503, retryable), as the pre-request gate's own failure does, and /readiness reports the watcher. With
    ALLOWED_HOSTS empty or * nothing changes.
  • A popup becomes readable one CDP round trip after it is reported. activate_tab waits 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_ephemeral session 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
    /readiness reports the watcher. With ALLOWED_HOSTS empty or * nothing changes and nothing attaches.
    What changes for legitimate popups:

    • a reported popup becomes readable one CDP round trip later than before (activate_tab waits 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.

  • The controller refuses requests from docker_ephemeral session 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 on 127.0.0.1 through host.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.

  • /docs and /redoc load 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 of bash -x traces.

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_PASSWORD on the shared browser-node, so its VNC-authentication checks run.