Skip to content

Audit 2026-10-04: password-field refusal, multi-monitor cursor, extension policy/injection fixes - #15

Merged
sheehanmunim merged 4 commits into
mainfrom
audit/2026-10-04
Oct 5, 2026
Merged

sheehanmunim merged 4 commits into
mainfrom
audit/2026-10-04

Conversation

@sheehanmunim

Copy link
Copy Markdown
Member

Fixes from the 2026-10-04 audit. Each one is small and has its own commit. Nothing is released or published.

Rust server (windows-linux)

  1. Password fields were never refused on Windows or Linux (e61762d). The type_text tool text and the README both say typing into password fields is refused by default (COMPUTER_USE_ALLOW_SECURE_FIELD_INPUT=1 turns it on), and the Swift server does refuse. The Rust server never checked. On Windows, set_value also echoed the value back into the transcript. It now matches Swift: when type_text gets an element_id, or set_value targets a password field (UIA IsPassword on Windows, AT-SPI PasswordText on Linux), it focuses the field and hands control back to the user.
    • Verified: cargo test on Windows, 93/93, including a new test for the flag and the message. The Linux build is left to this PR's ubuntu CI job, because no Linux toolchain was available.
  2. Real-pointer input could not reach a second monitor on Windows (03d7b1c). The fallback path for clicks, right-clicks, hovers, scrolls and drags used uiautomation's Mouse::move_to/click/right_click/drag_to. Those send MOUSEEVENTF_ABSOLUTE scaled by SM_CXSCREEN/SM_CYSCREEN without MOUSEEVENTF_VIRTUALDESK, which Windows maps onto the primary display only. Coordinates on other monitors were clamped to the primary display's edge. This is the problem already noted and fixed for remote control, except that remote drags still used drag_to. Every real-pointer move now uses SetCursorPos. Drag sends button down, stepped moves, then button up, and the button up is sent even if a move fails.
    • Verified: compiles and tests pass on Windows (cargo test -j 2 --locked), checked against the uiautomation 0.25.1 source and the SendInput docs. Not run against a real multi-monitor desktop.

Chrome extension

  1. Script injection through browser_click index (df79624). CLICK_JS put index verbatim into the script it evaluates in the page. The Rust bridge passes any JSON value through, so a string index ran as arbitrary page script through the debugger. That script could, for example, read a password field the snapshot deliberately hides. Only non-negative integers are accepted now.
  2. return_state read blocked sites (df79624). The snapshot taken after an action skipped the site rules. A link click, a redirect or a form submit that landed on a blocked site returned that page's content, even though the README says following a link into a blocked site does not get around the rule. The same checkTab that browser_snapshot uses now runs before the read.
  3. Stale snapshot indices (83ebab7). Old data-cu-idx tags were never cleared, so an element hidden since the previous snapshot kept its number. querySelector could then send a click or a credential fill to that hidden element.
  4. Lost debugger sessions (83ebab7). There was no chrome.debugger.onDetach listener. After the user cancelled Chrome's debugging bar, every later command on that tab failed until the tab was closed.
  5. Trailing-dot host bypass (83ebab7). https://bank.example./ slipped past a bank.example block rule. Trailing dots are now dropped from both hosts and patterns.

Verified: node chrome-extension/background.test.mjs passes 41/41. Each of the 5 new tests fails without its fix. scripts/build-extension.test.mjs and check-tool-parity.mjs pass, and tool-defs.mjs --check passes on an LF checkout.

Found but not fixed (for the owner)

  • Windows named pipes (bridge and .rpc) use the default DACL, which gives Everyone read access. The pipe name munim-computer-use-bridge-<USERNAME> is predictable. On a multi-user Windows machine, another local user could create that pipe first. The victim's server would then join as a peer, sending its browser commands to that user. The victim's native host would connect to it too, letting that user drive the victim's signed-in Chrome. Fixing this needs an owner-only security descriptor plus a check of the pipe server's identity (GetNamedPipeServerProcessId and its token SID). I could not test this across users here. Unix sockets are fine (0600 inside a 0700 directory).
  • The app policy fails open in check_app_policy (main.rs). If list_apps() errors, the rule is matched against the raw query string. A numeric pid then only hits the * rule, so a pid can get around a name-specific block. Failing closed would block every app on sessions where xcap cannot list windows (WSLg), so this is left as a decision for you.
  • The process is not DPI-aware on Windows, except for the live-capture thread. Coordinates on scaled displays were not verified.
  • Extension, credentials:
    • Values filled through browser_request_credentials can come back to the agent. A page's "show password" toggle turns the field into type=text, and username and code values are always shown as snapshot labels.
    • The model picks the kind of each field.
    • request_credentials is allowed on data: and about: pages, where the origin is null.
  • Extension, approvals and listeners: approvedOrigins is never cleared per client, so "ask" approvals outlive the task. The per-tab badge onUpdated listener leaks one listener per closed tab.

🤖 Generated with Claude Code

sheehanmunim and others added 4 commits October 4, 2026 22:04
The type_text tool description and the README promise that typing into
password fields is refused by default (COMPUTER_USE_ALLOW_SECURE_FIELD_INPUT=1
opts in), and the macOS server does refuse. The Rust server never checked, so
on Windows and Linux an agent could type or set_value a credential into a
password field, and Windows set_value echoed the value back into the
transcript.

Mirror the Swift behaviour: when type_text is given an element_id, or
set_value targets an element, that is a password field (UIA IsPassword on
Windows, AT-SPI role PasswordText on Linux), focus it for the user and hand
control back instead of writing into it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When a click, right-click, hover, scroll or drag had to fall back to the real
pointer (Chromium, WPF, UWP and anything else that ignores posted messages),
the agent path used uiautomation's Mouse::move_to / click / right_click /
drag_to. Those send absolute SendInput moves normalised against
SM_CXSCREEN/SM_CYSCREEN without MOUSEEVENTF_VIRTUALDESK, which Windows maps
onto the primary display only, so any coordinate on a second monitor (or left
of / above the primary) landed clamped at the primary display's edge. The
remote-control path had already been moved to SetCursorPos for this reason,
but its drag still finished with drag_to and had the same problem.

Use SetCursorPos (jump_cursor) for every real-pointer move, and drag with an
explicit button down, stepped SetCursorPos moves and a button up that is sent
even if a move fails.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… reads

Two ways an agent could get past the guarantees the extension makes:

- browser_click's index was spliced verbatim into the script CLICK_JS
  evaluates in the page. The macOS server only forwards an integer, but the
  Windows/Linux server forwards whatever JSON the tool call carried, so a
  string index ran as arbitrary page script through the debugger (for
  example reading a password field the snapshot deliberately hides).
  clickElement now accepts only a non-negative integer.

- return_state snapshots were taken after the action without applying the
  site rules, so a click on a link, a redirecting navigate or a submitted
  form that landed on a blocked site returned that site's page to the
  model. The README promises following a link into a blocked site does not
  get around the rule. withState now runs the same checkTab
  browser_snapshot runs before reading.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ot hosts

- The snapshot script tags elements with data-cu-idx but never cleared the
  tags an earlier snapshot left. An element hidden since then (an earlier
  step of a multi-step form) kept its old number, and querySelector returns
  the first match, so a click or a credential fill could go to that hidden
  element instead of the one the agent was shown. Clear old tags first.

- There was no chrome.debugger.onDetach listener. When Chrome ended a
  session itself (the user pressing Cancel on the "is debugging this
  browser" bar, DevTools taking the tab), the tab stayed in `attached`,
  attach() skipped re-attaching and every later command on that tab failed
  until it was closed or the worker restarted.

- Site rules compared hostnames verbatim, so https://bank.example./ (same
  site, trailing dot) matched neither "bank.example" nor its subdomains and
  slipped past a block rule. Trailing dots are now dropped from both hosts
  and patterns.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 02:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sheehanmunim
sheehanmunim merged commit deb4c7a into main Oct 5, 2026
4 checks passed
@sheehanmunim
sheehanmunim deleted the audit/2026-10-04 branch October 5, 2026 12:20
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