Skip to content

v1.14.0

Choose a tag to compare

@github-actions github-actions released this 04 Nov 16:47
· 12 commits to main since this release

A hardening release over 1.13.1. The controller no longer runs as root, and Chromium runs with its own sandbox in
browser-node and in every isolated container, so a renderer exploit no longer starts with every syscall the
container allows. BROWSER_SANDBOX=on refuses any session whose browser is not verifiably sandboxed. Read the
upgrade notes first: the controller changes user and hands its data over at the first start, and browser-node runs
under a custom seccomp profile. The controller and browser-node images change; the client, LangChain and MCP bridge
packages change only in version.

Upgrade notes

  • A default compose install now runs browser-node under a custom seccomp profile that lets the container create
    user namespaces.
    Chromium's own sandbox needs them, and Docker's default profile refuses them to a container
    without CAP_SYS_ADMIN. The profile is moby's default plus chroot, clone and unshare
    (controller/app/browser/seccomp/); docker_ephemeral containers get it too. It exposes the kernel's
    user-namespace code to code in the container, and in exchange every renderer runs sandboxed. That is the better
    trade for a browser that renders arbitrary pages: a renderer exploit no longer starts with every syscall the
    container allows. To keep the old setup under compose, set both BROWSER_SANDBOX=off and
    BROWSER_SECCOMP_PROFILE=builtin (docs/deployment.md, "Chromium's sandbox"). On a host whose Docker daemon sets
    its own seccomp-profile, 1.14.0's profile replaces that one, and builtin means moby's built-in profile, not the
    daemon's: point BROWSER_SECCOMP_PROFILE and ISOLATED_BROWSER_SECCOMP_PROFILE at your profile plus the
    auto-browser entry to keep it.
  • The controller now runs as the owner of your data directory, or as uid 10002, not as root. A Linux checkout
    whose ./data belongs to you runs it as you; Docker Desktop and named volumes run it as uid 10002 (never
    browser-node's 10001). It holds no capabilities and runs with no_new_privs. At start, the
    root-owned files earlier releases left in the controller's own /data subtrees are handed to that user, and
    data/api-token stays readable by its owner. CONTROLLER_RUN_AS=root restores the root controller; it is not
    recommended, and readiness warns while it is set. With the isolation overlay on Docker Desktop, the controller
    joins group 0 to use the Docker socket.
  • BROWSER_SANDBOX=on now also gates shared mode: every session is refused, and readiness fails, while the
    shared browser-node's connect-time state is not a verified on, including a browser-node image older than 1.14.0.
    Under on, browser.cdp_attach is refused; a CDP_CONNECT_URL Chrome is the operator's own and is reported as
    attached, not checked.
  • Rolling back: after running an older release on the same data, delete data/.auto-browser-owner before
    starting 1.14 again, so the files the older release created as root are handed over. A container without
    DAC_OVERRIDE or DAC_READ_SEARCH cannot look inside its own unreadable directories: chown ./data by hand then.
  • A capability-less uid 0 still owns the image: under cap_drop: [SETUID] or [SETGID], or a cap_add set
    without SETUID or SETGID, a compromised controller could rewrite the image and regain the container's capabilities
    on a restart. The entrypoint names them; prefer cap_drop: [ALL], user: "10002:10002", or read_only: true
    with a tmpfs for /tmp, which screenshot OCR needs (docs/deployment.md).
  • A controller that cannot switch users (cap_drop: [ALL], cap_drop: [SETUID] or [SETGID], or cap_add
    without SETUID or SETGID) starts as a uid 0 with no capabilities at all, and refuses to start, in one line, when
    it cannot enter the API token's directory or read the token the controller would read. A directory it cannot see,
    not there yet or under one it cannot search, is left to the controller, which creates it or says why it cannot.
    Run it as user: "10002:10002", or add cap_add: [CHOWN, DAC_READ_SEARCH, SETUID, SETGID] (docs/deployment.md).
    Data owned by 65534 (root-squashed NFS) runs as 65534.

Security

  • Chromium runs with its own sandbox in browser-node and every isolated container (BROWSER_SANDBOX=auto, the
    default). It launched with the sandbox off, so a renderer exploit ran with every syscall the container allowed.
    browser-node now checks in /proc that renderers really are sandboxed; where a host refuses the user namespace
    (Ubuntu 23.10 and later restrict it with AppArmor), auto runs Chromium unsandboxed with a WARNING that names the
    cause, on refuses to start, and off keeps the old behaviour. Readiness reports every browser's state.
  • The controller no longer runs as root (see the upgrade notes).
  • A closed isolated session's profile and downloads are removed. A controller that is not root could not delete
    the browser user's files, and the error was ignored, so the session's cookies stayed on disk; they are now emptied
    as the browser user, and anything left is a WARNING with its path.
  • The controller reads the browsers' endpoint files without following links, and no error quotes their
    content: a link planted at a Playwright endpoint file could have put a file the controller can read, its API token
    included, into its log. An endpoint is a short ws:// or wss:// URL in printable ASCII, or it is refused.

Fixed

  • When screenshot PII scrubbing cannot run, the WARNING (and the error, under PII_SCRUB_SCREENSHOT_STRICT=true)
    says what its reason needs.
    Both said to run the Docker image, which ships tesseract, even when the controller
    already ran it and its OCR run failed, for example for want of a writable temporary directory under
    read_only: true. A failed OCR run now names that check, OCR_ENABLED=false names the setting, and the cause of
    the failure is logged at DEBUG.

Added

  • A weekly CI check (scripts/check_chromium_freshness.py) fails when Chrome stable for Linux is a major ahead of the
    Chromium the pinned Playwright bundles, when a newer Playwright bundles a newer Chromium, or when moby's default
    seccomp profile has moved away from the one the sandbox profile is built from.

Changed

  • CI's compose-smoke prints each browser's measured sandbox state, and checks that the controller runs as the owner
    of ./data.