Skip to content

fix(browser): block sensitive CDP state reads - #54198

Open
timsmykov wants to merge 5 commits into
NousResearch:mainfrom
timsmykov:tim/browser-cdp-sensitive-state-guard-20260628
Open

timsmykov wants to merge 5 commits into
NousResearch:mainfrom
timsmykov:tim/browser-cdp-sensitive-state-guard-20260628

Conversation

@timsmykov

Copy link
Copy Markdown
Contributor

Summary

Blocks raw browser credential/storage reads from the browser_cdp escape hatch before any Chrome DevTools Protocol request is dispatched.

This closes the highest-risk CDP surfaces that can dump authenticated browser state directly into model/tool context:

  • Network.getAllCookies / Network.getCookies
  • Storage.getCookies
  • explicit DOM Storage, IndexedDB, and CacheStorage read methods
  • Runtime.evaluate / Runtime.callFunctionOn source that reads document.cookie, localStorage, or sessionStorage

Allowed non-read storage methods still pass through, so the tool remains useful for browser automation and low-level debugging.

Why

browser_cdp is intentionally a low-level escape hatch. That makes it useful, but it also bypasses the safer high-level browser tool surfaces. Cookie and Web Storage reads can expose session tokens or other browser secrets and then persist those values in transcripts. This PR blocks those calls before WebSocket dispatch, so the sensitive values never enter model context.

Notes

  • The guard is pre-dispatch: blocked calls never reach the CDP mock/server.
  • Runtime source scanning catches direct property reads and common literal-splitting forms such as document['co' + 'okie'].
  • The schema no longer advertises Network.getAllCookies as an example and documents the blocked surfaces.

Tests

  • python -m compileall -q tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • python -m pytest tests/tools/test_browser_cdp_tool.py -q -o 'addopts='
  • python -m ruff check tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py

@timsmykov
timsmykov force-pushed the tim/browser-cdp-sensitive-state-guard-20260628 branch from 7a152f1 to 7d0cb81 Compare June 28, 2026 11:02
@rodriguez46p-ui

Copy link
Copy Markdown

Hourly commander review note: the new CDP guard passes the focused suite locally, but I found one small bypass worth fixing before marking the PR ready.

Runtime.evaluate with optional chaining can still read cookies:

from tools.browser_cdp_tool import _runtime_source_reads_sensitive_state
assert _runtime_source_reads_sensitive_state('document.cookie') is True
assert _runtime_source_reads_sensitive_state('document?.cookie') is True  # currently False

Local verification from a detached worktree at 7a152f195:

  • python -m compileall -q tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • python -m pytest tests/tools/test_browser_cdp_tool.py -q -o 'addopts='43 passed
  • python -m ruff check tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • ad-hoc scanner probe: document?.cookie returned False while document.cookie, window?.localStorage, and globalThis?.sessionStorage returned True.

Suggested fix: include optional chaining in the document.cookie detector, e.g. allow document\s*\??\.\s*cookie, and add a regression case for document?.cookie.

@tonydwb tonydwb 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.

Code Review Summary\n\nVerdict: LGTM\n\nSecurity fix that blocks sensitive browser state reads (cookies, localStorage, sessionStorage) from entering model context via CDP. Well-scoped with comprehensive test coverage (160 lines of tests covering method blocking, expression pattern detection, and obfuscation normalization).\n\n- Clean allowlist/blocklist approach for CDP methods\n- Regex patterns correctly handle JS string concatenation and unicode escapes\n- Moved validation before CDP dispatch (correct order of operations)\n- Updated usage docs to reflect the new blocking behavior\n- Test parametrization covers edge cases well\n\n---\nReviewed by Hermes Agent

@timsmykov
timsmykov marked this pull request as ready for review June 28, 2026 11:09
Copilot AI review requested due to automatic review settings June 28, 2026 11:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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.

@alt-glitch alt-glitch added type/security Security vulnerability or hardening tool/browser Browser automation (CDP, Playwright) sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data P2 Medium — degraded but workaround exists labels Jun 28, 2026
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

Related: this competes with #46899 for hardening the browser_cdp escape hatch. #54198 blocks sensitive read methods (cookies/DOM-storage/IndexedDB/CacheStorage + Runtime.evaluate reads of document.cookie/localStorage) pre-dispatch with no escape hatch; #46899 is a broader default-deny guard (also permission changes, storage clear, script inject/eval) with a browser.allow_sensitive_cdp_methods escape hatch. Same file (tools/browser_cdp_tool.py), different mechanism — flagging the cluster so a maintainer picks one. Also related: #52349 (CDP cloud-metadata SSRF floor), #5294 (CDP-URL log redaction), #50042 (browser-export redaction).

@rodriguez46p-ui

Copy link
Copy Markdown

Hourly commander follow-up on the latest head 7d0cb8198: the PR is now marked ready for review, but the optional-chaining cookie bypass from my earlier note is still reproducible.

Local focused checks in a detached worktree:

  • python -m compileall -q tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • python -m pytest tests/tools/test_browser_cdp_tool.py -q -o 'addopts='43 passed
  • python -m ruff check tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • ad-hoc scanner probe ❌:
document.cookie: got=True expected=True
document?.cookie: got=False expected=True
document ?. cookie: got=False expected=True
document["cookie"]: got=True expected=True
document?.["cookie"]: got=False expected=True
window?.localStorage: got=True expected=True
globalThis?.sessionStorage: got=True expected=True

The current pattern is still document\s*\.\s*cookie, so document?.cookie / document?.["cookie"] can reach Runtime.evaluate and return raw cookies to the transcript. I would keep this blocked until the regex/test coverage includes JS optional chaining for cookie reads.

@tonydwb tonydwb 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.

Security hardening (275 additions). Blocks CDP methods that expose cookies, localStorage, sessionStorage, and IndexedDB. Includes JS obfuscation normalization (string concatenation, unicode escapes) to prevent bypass. Comprehensive test coverage with parametrized tests.

Reviewed by Hermes Agent

@timsmykov

Copy link
Copy Markdown
Contributor Author

Fixed the optional-chaining cookie-read bypass in c2cbfdc. The Runtime source scanner now catches document?.cookie / spaced optional chaining / document?.["cookie"], with regression coverage for Runtime.evaluate and Runtime.callFunctionOn.

Verified locally:

  • python -m compileall -q tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • python -m pytest tests/tools/test_browser_cdp_tool.py -q -o 'addopts=' — 49 passed
  • python -m ruff check tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • ad-hoc probe confirms the reported optional-chaining cases now return True without flagging document.title

@rodriguez46p-ui

Copy link
Copy Markdown

Hourly commander follow-up on head c2cbfdc40: the optional-chaining cookie fix looks good in the focused probe, but I found a sibling sensitive-state gap before calling the PR fully cleared.

Local focused checks in a detached worktree:

  • python -m compileall -q tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • python -m pytest tests/tools/test_browser_cdp_tool.py -q -o 'addopts=' ✅ (49 passed)
  • python -m ruff check tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • Ad-hoc optional-chaining probe ✅ for document?.cookie, document ?. cookie, document?.["cookie"], window?.localStorage, and globalThis?.sessionStorage

New issue: the PR blocks explicit IndexedDB.* / CacheStorage.* CDP read methods, but equivalent Runtime.evaluate reads still dispatch and can return those values to model context:

indexedDB.databases() blocked=False contains_secret=True received_calls=2
window.indexedDB.databases() blocked=False contains_secret=True received_calls=4
caches.keys() blocked=False contains_secret=True received_calls=6
window.caches.keys() blocked=False contains_secret=True received_calls=8

That means a model can bypass the new IndexedDB.requestDatabaseNames / CacheStorage.requestCacheNames method guards by asking Runtime to execute the browser APIs directly. If the intent is to keep raw browser storage state out of immutable model/tool transcripts, the Runtime source scanner should probably cover indexedDB and caches/CacheStorage access too (including window/globalThis/self and bracket/optional-chain variants), with regression tests similar to the cookie/storage ones.

@timsmykov

timsmykov commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

Fixed the Runtime.evaluate / Runtime.callFunctionOn IndexedDB + CacheStorage bypass in 0d0750ab1.

What changed:

  • Runtime source scanner now blocks direct, prefixed, optional-chain, bracket, and simple string-concat access for indexedDB, caches, and CacheStorage.
  • Covered window, globalThis, and self forms.
  • Kept safe expressions like document.title allowed.

Verified locally:

  • python -m compileall -q tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • python -m pytest tests/tools/test_browser_cdp_tool.py -q -o 'addopts=' — 68 passed
  • python -m ruff check tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • ad-hoc scanner probe confirms the reported indexedDB.databases() / window.indexedDB.databases() / caches.keys() / window.caches.keys() forms now return True, while document.title remains False
  • local git merge-tree against upstream/main: clean

@rodriguez46p-ui

Copy link
Copy Markdown

Hourly commander follow-up on head 0d0750ab1: the IndexedDB/CacheStorage Runtime fix covers the previously reported cases, but I found one remaining cookie-read syntax variant before calling this fully cleared.

Local focused checks in a detached worktree:

  • python -m compileall -q tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • python -m pytest tests/tools/test_browser_cdp_tool.py -q -o 'addopts=' ✅ (68 passed)
  • python -m ruff check tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • gh pr checks 54198 --repo NousResearch/hermes-agent reports no hosted checks for the branch

Previously reported Runtime storage cases now pass the scanner (indexedDB.databases(), window.indexedDB.databases(), caches.keys(), window.caches.keys(), plus optional-chain/bracket variants). The remaining gap is JavaScript template-literal bracket access for document.cookie:

document[`cookie`]: got=False expected=True
document?.[`cookie`]: got=False expected=True
document[`co${"okie"}`]: got=False expected=True
document?.[`co${"okie"}`]: got=False expected=True
document["cookie"]: got=True expected=True
document?.["cookie"]: got=True expected=True
indexedDB.databases(): got=True expected=True
window[`indexedDB`].databases(): got=True expected=True
caches.keys(): got=True expected=True
self?.[`caches`].keys(): got=True expected=True
document.title: got=False expected=False

Because document[cookie] and document?.[cookie] are valid JS and read the same raw cookie string as document["cookie"], they can still reach Runtime.evaluate/Runtime.callFunctionOn and return cookies into tool output. I would add backtick-template bracket coverage for the document.cookie scanner/tests as well.

@timsmykov

Copy link
Copy Markdown
Contributor Author

Fixed the remaining template-literal sensitive-state bypass in ba397a10c.

Why this was worth fixing:

  • Reproduced the report before the fix: _runtime_source_reads_sensitive_state('document[cookie]') and document?.[cookie] returned False.
  • Confirmed impact with the in-process CDP mock: Runtime.evaluate dispatched and the mocked cookie value came back in the tool result, so this was a real model-context leak path, not just a theoretical scanner mismatch.

What changed:

  • Bracket-property sensitive-state patterns now accept backtick-delimited template literals for document['cookie'], web storage, IndexedDB, CacheStorage, and caches.
  • Simple JS identifier string-concat normalization now handles backtick-delimited strings too, e.g. document[co+okie] and window[index+edDB].
  • Added regression coverage for Runtime.evaluate and Runtime.callFunctionOn template-literal variants.

Verification:

  • Before code fix, the new regression cases failed as expected: 6 failures, with Runtime.evaluate / Runtime.callFunctionOn returning secret-browser-state instead of blocking.
  • Post-fix ad-hoc probe: reported template-literal forms return True, document.title remains False, CDP call count stays 0, and no mocked secret leaks.
  • python3 -m compileall -q tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py
  • python3 -m pytest tests/tools/test_browser_cdp_tool.py -q -o 'addopts='83 passed
  • python3 -m ruff check tools/browser_cdp_tool.py tests/tools/test_browser_cdp_tool.py

@alt-glitch alt-glitch added P3 Low — cosmetic, nice to have needs-repro Bug needs reproduction steps and removed P2 Medium — degraded but workaround exists sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Jul 14, 2026
@teknium1

Copy link
Copy Markdown
Collaborator

Thanks for the focused pre-dispatch guard and the follow-up fixes from the review thread. The underlying issue is present on current main: browser_cdp still dispatches arbitrary methods after only parameter and private-page checks (tools/browser_cdp_tool.py:478-501).

Problems

  • Final head a70f3a24 removes the Runtime indexedDB/caches patterns and their tests, even though the earlier review demonstrated that Runtime.evaluate was a sibling path around the explicit IndexedDB.* / CacheStorage.* method blocks. That leaves the direct-method guard bypassable through runtime source.
  • The new scanner is not a complete JavaScript boundary. For example, document[co${"okie"}] resolves to document.cookie, but is not handled by the added literal-concatenation normalization or bracket-property patterns. The added tests do not cover interpolation.
  • Current browser_console already centralizes a broader sensitive-evaluation policy, including IndexedDB and Cache Storage (tools/browser_tool.py:3415-3534). The CDP path should not diverge from that policy without an explicit scoped decision.

Suggested changes

  • Restore Runtime IndexedDB/CacheStorage coverage and add pre-dispatch tests for both Runtime methods.
  • Add the template-interpolation regression.
  • Consolidate the two evaluation policies, including the intended compatibility/opt-in behavior.

Automated hermes-sweeper review.

@alt-glitch alt-glitch added the sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data label Jul 15, 2026
@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-repro Bug needs reproduction steps P3 Low — cosmetic, nice to have sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/browser Browser automation (CDP, Playwright) type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants