Skip to content

fix(security): tighten IPC socket perms + use constant-time token compare#331

Open
aaronjmars wants to merge 1 commit into
browser-use:mainfrom
aaronjmars:security/ipc-socket-perms-and-token-compare
Open

fix(security): tighten IPC socket perms + use constant-time token compare#331
aaronjmars wants to merge 1 commit into
browser-use:mainfrom
aaronjmars:security/ipc-socket-perms-and-token-compare

Conversation

@aaronjmars
Copy link
Copy Markdown

@aaronjmars aaronjmars commented May 10, 2026

Summary

Two small defense-in-depth fixes for the daemon IPC plumbing, bundled because they share the same threat surface (the daemon's IPC entry point) and the tests live next to each other.

  1. POSIX AF_UNIX socket race. _ipc.serve() chmod'd the socket file after asyncio.start_unix_server() returned, but start_unix_server begins accepting connections immediately. Under the default umask 0o022 the socket file is briefly mode 0o755 — a co-located unprivileged user could connect() during that window and start issuing CDP commands. Fix: tighten umask to 0o077 around start_unix_server so bind() creates the socket with owner-only perms atomically. The explicit chmod 0o600 stays as a belt-and-braces follow-up.

  2. Windows TCP loopback token compare. Daemon.handle() used req.get("token") != expected, which short-circuits at the first differing byte. The token is secrets.token_hex(32) (256 bits) so the practical timing leak on loopback is small, but hmac.compare_digest is one import and constant-time. Fix: type-check the received value (req is parsed JSON, so any shape can land in req["token"]), then compare with hmac.compare_digest.

Impact

Both findings are local-only / loopback-only with high attacker-side cost — calling them low severity. Worth fixing because:

  • (1) is a real race window on multi-tenant boxes (CI runners, shared dev VMs). The attack yields full control of the user's browser session via CDP — cookies, navigation, form fill, DOM read.
  • (2) is defense-in-depth for an attack that's hard to land on async loopback in practice, but trivial to fix.

Location

  • src/browser_harness/_ipc.py:147-167serve() POSIX path
  • src/browser_harness/daemon.py:259-271Daemon.handle() token guard

Fix

  • _ipc.serve(): os.umask(0o077) around await asyncio.start_unix_server(...), restore in a finally. Existing os.chmod(path, 0o600) kept as a defence in depth for backends that historically didn't honour umask on AF_UNIX bind().
  • daemon.Daemon.handle(): add hmac to module imports; replace the != check with isinstance(received, str) and hmac.compare_digest(received, expected). The isinstance gate is required because compare_digest raises TypeError on non-str inputs, and req is parsed JSON.

Detected by

Aeon + semgrep + manual review.

  • Severity: low (both findings)
  • CWE: CWE-732 (incorrect permission assignment), CWE-208 (observable timing discrepancy)

Verification

python3 -m pytest tests/unit/79/79 pass (74 baseline + 5 new):

  • test_serve_posix_socket_is_created_with_owner_only_perms — spies asyncio.start_unix_server to capture the active umask at bind-time + spies os.chmod to confirm the 0o600 follow-up
  • test_handle_token_compare_uses_constant_time_compare — spies hmac.compare_digest to assert the constant-time path is used
  • test_handle_rejects_non_string_token_without_crashing — covers None, ints, bools, lists, dicts, bytes
  • test_handle_accepts_correct_token — sanity check the right token still passes
  • test_handle_skips_token_check_on_posix_when_expected_is_none — confirms POSIX path stays a no-op (AF_UNIX + chmod 600 is the boundary there)

No production behaviour change for legitimate clients. Existing helpers.py:_send flows already pass the token via ipc.connect/ipc.request, which is unchanged.

Notes


Filed by Aeon.


Summary by cubic

Hardened the daemon IPC entry point by creating POSIX sockets with owner-only permissions atomically and using constant-time token comparison on Windows. Fixes a short socket-perms race and removes a small timing side-channel; no impact to legitimate clients.

  • Bug Fixes
    • POSIX AF_UNIX: set umask(0o077) around asyncio.start_unix_server(...); keep an explicit chmod 0o600 as a follow-up.
    • Windows loopback: type-check the token and compare with hmac.compare_digest; return unauthorized on mismatch.

Written for commit 327f4aa. Summary will update on new commits.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

…pare

Two small defense-in-depth fixes for the daemon IPC plumbing in _ipc.py
and daemon.py:

1. _ipc.serve() (POSIX): asyncio.start_unix_server() begins accepting
   connections immediately, but the previous code only chmod'd the
   AF_UNIX socket file *after* bind() returned. Under the default
   umask 0o022 the file was briefly mode 0o755, so a co-located
   unprivileged user could connect() during that window and start
   issuing CDP commands (control the user's browser session, read
   cookies, navigate to internal URLs, etc.). Tighten umask to
   0o077 around start_unix_server so bind() creates the file with
   owner-only perms atomically; the explicit chmod 0o600 stays as a
   belt-and-braces follow-up for backends that historically ignored
   umask on AF_UNIX bind().

2. daemon.Daemon.handle() (Windows): the TCP-loopback token comparison
   used `req.get("token") != expected`, which short-circuits at the
   first differing byte. The token is 64 hex chars (256 bits of
   entropy) so the practical timing leak is small, but use
   hmac.compare_digest as a constant-time compare since it costs one
   import. Also type-check the received value first because
   compare_digest raises TypeError on non-str inputs (req is parsed
   JSON so the field can be any shape).

Detected by Aeon + semgrep + manual review.
Severity: low (both findings local-only / loopback-only, with high
attacker-side cost). Bundled because they share the same threat
surface (the daemon IPC entry point) and tests live next to each
other in tests/unit/.

Verification: `python3 -m pytest tests/unit/` passes 79/79 (5 new
tests: socket-perm spy on umask + chmod, four token-compare cases —
constant-time-compare invocation, non-string rejection, correct-token
acceptance, POSIX no-op short-circuit).
@aaronjmars aaronjmars force-pushed the security/ipc-socket-perms-and-token-compare branch from fc069e2 to 327f4aa Compare May 10, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants