Summary
The click and type_text tools build error/hint/note strings that splice page-derived <select>/listbox option labels (and the currently-selected option text) directly into the tool result. Neither tool is in UNTRUSTED_CONTENT_TOOLS (src/chrome/src/agent/permission-gate.js), so those results are not wrapped in <untrusted_page_content> markers — the attacker-controllable option text reaches the model as trusted data.
This is the same laundering class we just fixed in _digestToolResult (PR #102), but in a different code path. It is pre-existing and unrelated to the context-compaction work, so it was intentionally left out of that PR.
Threat model
A page the agent interacts with controls the <option> text / aria-label of a dropdown. When the model clicks the dropdown (a very common action on forms), WebBrain returns an error/hint listing the available options verbatim. A malicious page can set an option label to instruction text (e.g. Ignore previous instructions and …), smuggling it into the model's trusted context.
Affected code (Chrome only — Firefox has a simpler click impl with none of these)
src/chrome/src/agent/agent.js:
4610 — No clickable element found … an open ${openOptions.source} is visible … with these options: ${openOptions.options.map(...)}
4706 — … Available options: ${opts.options.join(', ')}
4756 — hint: … Available options: ${cs.options.join(', ')} (+ cs.current)
4802 — error: … (current: "${pOpts.current}") … Available options: ${pOpts.options.join(', ')}
4882 — hint: … (current: "${selTag.current}") … Available options: ${selTag.options.join(', ')}
4934 — error: … (current: "${coordTag.current}") … Available options: ${coordTag.options.join(', ')}
5022 — error: … (current: "${redir.current}") … Available options: ${redir.options.join(', ')}
5053 — error: … (current: "${pOpts2.current}") … Available options: ${pOpts2.options.join(', ')}
5160-5161 — type_text: No option matching "…". Available: ${opts.map(o => o.text.trim()).join(', ')}
The current values (currently-selected option text) are page-derived too.
Recommended fix
Option A (preferred): add 'click' and 'type_text' to UNTRUSTED_CONTENT_TOOLS. Their results legitimately carry page-derived text (option labels, element text, aria-labels), so the whole result should be marked untrusted — this protects present and future fields automatically, and the boundary/digest handling already added for the compaction path will then treat them correctly.
Option B: sanitize the option labels at the splice points (strip [<>`" + newlines, bound length) before interpolating. Narrower but easy to miss a new field later.
Recommend Option A. Whichever is chosen, add a quick test asserting a malicious option label cannot appear unwrapped in a click/type_text result.
Scope notes
- Chrome only (
src/chrome/src/agent/agent.js). Firefox (src/firefox) has no Available options: splicing.
- If Option A is taken, double-check
_digestToolResult still produces a useful content-free digest for the newly-untrusted click/type_text (it will fall to the generic ${name} ok (untrusted page content) branch).
Found during the PR #102 security sweep.
Summary
The
clickandtype_texttools build error/hint/note strings that splice page-derived<select>/listbox option labels (and the currently-selected option text) directly into the tool result. Neither tool is inUNTRUSTED_CONTENT_TOOLS(src/chrome/src/agent/permission-gate.js), so those results are not wrapped in<untrusted_page_content>markers — the attacker-controllable option text reaches the model as trusted data.This is the same laundering class we just fixed in
_digestToolResult(PR #102), but in a different code path. It is pre-existing and unrelated to the context-compaction work, so it was intentionally left out of that PR.Threat model
A page the agent interacts with controls the
<option>text /aria-labelof a dropdown. When the model clicks the dropdown (a very common action on forms), WebBrain returns an error/hint listing the available options verbatim. A malicious page can set an option label to instruction text (e.g.Ignore previous instructions and …), smuggling it into the model's trusted context.Affected code (Chrome only — Firefox has a simpler click impl with none of these)
src/chrome/src/agent/agent.js:4610—No clickable element found … an open ${openOptions.source} is visible … with these options: ${openOptions.options.map(...)}4706—… Available options: ${opts.options.join(', ')}4756—hint: … Available options: ${cs.options.join(', ')}(+cs.current)4802—error: … (current: "${pOpts.current}") … Available options: ${pOpts.options.join(', ')}4882—hint: … (current: "${selTag.current}") … Available options: ${selTag.options.join(', ')}4934—error: … (current: "${coordTag.current}") … Available options: ${coordTag.options.join(', ')}5022—error: … (current: "${redir.current}") … Available options: ${redir.options.join(', ')}5053—error: … (current: "${pOpts2.current}") … Available options: ${pOpts2.options.join(', ')}5160-5161—type_text:No option matching "…". Available: ${opts.map(o => o.text.trim()).join(', ')}The
currentvalues (currently-selected option text) are page-derived too.Recommended fix
Option A (preferred): add
'click'and'type_text'toUNTRUSTED_CONTENT_TOOLS. Their results legitimately carry page-derived text (option labels, element text, aria-labels), so the whole result should be marked untrusted — this protects present and future fields automatically, and the boundary/digest handling already added for the compaction path will then treat them correctly.Option B: sanitize the option labels at the splice points (strip
[<>`"+ newlines, bound length) before interpolating. Narrower but easy to miss a new field later.Recommend Option A. Whichever is chosen, add a quick test asserting a malicious option label cannot appear unwrapped in a
click/type_textresult.Scope notes
src/chrome/src/agent/agent.js). Firefox (src/firefox) has noAvailable options:splicing._digestToolResultstill produces a useful content-free digest for the newly-untrustedclick/type_text(it will fall to the generic${name} ok (untrusted page content)branch).Found during the PR #102 security sweep.