feat(cdp,cli): driver ergonomics — wait/cookies/storage/animations - #215
Conversation
#75 #76 #77) Bundles four CDP-side V1 driver-ergonomics features that all extend the same `Target` and `ChromiumOptions` types in `plumb-cdp`. Splitting would have forced three rebases on the same struct. #74 (--wait-for / --wait-ms): adds `Target::wait_for_selector` and `Target::wait_ms`. The driver runs them after `wait_for_navigation`, selector first then the additional sleep, gated by a 10s budget under `tokio::time::timeout` to avoid leaking wall-clock into determinism. #75 (--cookie / --header / --auth-script): adds `ChromiumOptions::cookies`, `ChromiumOptions::headers`, `ChromiumOptions::auth_script`. Cookies and headers are control-character-validated at parse time and sorted before injection so CDP traffic is byte-stable across runs. Auth script paths are canonicalized and refused when they resolve outside CWD. #76 (--storage-state): adds `ChromiumOptions::storage_state` and a `StorageState` parser with `serde(deny_unknown_fields)`. Cookies in the file install before navigation; localStorage entries write via `page.evaluate` after navigation when the origin matches. Cookies are sorted by `(domain, name)` and origins by `origin` for deterministic injection order. #77 (--disable-animations / --hide-scrollbars / --dpr): adds three new per-target capture knobs. `disable_animations` and `hide_scrollbars` default to `true` and inject CSS via `Page.addScriptToEvaluateOnNewDocument`. `pin_dpr` overrides the viewport's `device_pixel_ratio` when set, applied through the existing `Emulation.setDeviceMetricsOverride` call. The `pre_navigate` helper centralizes the "set up the page before goto" stages — animation killer, scrollbar killer, auth script, headers, cookies, storage-state cookies — so `ChromiumDriver` and `PersistentBrowser` share one implementation. CLI flags wire through the new `LintArgs` struct in `plumb_cli::commands::lint`. Existing 25 integration tests still pass; 8 new ones cover argument parsing, validation errors, and successful fake-driver flows for every flag family. Closes #74, #75, #76, #77. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
I now have enough context to write the full review. Review: PR #215 — CDP Driver Ergonomics + Security Wave (H1-H3, M1-M4)Files reviewed: Bucket: 1. Determinism invariants[REQUEST CHANGES] Fix options — pick one:
The second option is acceptable given the documented security rationale, but it must be recorded or the rule and the code are in contradiction. No other determinism violations found. 2. Workspace layeringNo violations. 3. Error handlingNo
[WARNING] [WARNING] 4. Test coverageGood overall. The new CLI tests in
The unit tests in [WARNING] [WARNING] 5. Documentation
[REQUEST CHANGES] Missing strict-parsing note for [WARNING] 6. Dependencies[WARNING] Punch list
Items 1 and 2 require code or doc changes before merge. Items 3–7 are improvements that don't block the security fix. Verdict: REQUEST_CHANGES |
Load `StorageState::load_from_path` exactly once in `pre_navigate`, parse it, and thread the parsed value through to both `install_storage_state_cookies` (synchronously, pre-navigation) and `apply_storage_state_local_storage` (post-navigation). Previously, the storage-state file was opened and parsed twice — once for cookie installation and once for localStorage replay — opening a TOCTOU window where the file content could change between the two reads. Loading once and threading the parsed value through closes that window without changing observable behavior. Addresses 06-security-auditor finding H1 on PR #215.
Document that `canonicalize_safe_path`, `inject_auth_script`, and `StorageState::load_from_path` rely on a canonicalize-then-open sequence that has an inherent TOCTOU window. A co-located attacker with write access to a parent directory of the supplied path can swap the resolved file for a symlink between the check and the subsequent read. The check still prevents accidental path issues (typos, absolute paths outside CWD), which is what the CLI flags need it for. The full mitigation requires `cap_std::Dir::open` and is out of scope for this wave; the doc comment exists so future maintainers don't assume guarantees that aren't there. Addresses 06-security-auditor finding H2 on PR #215.
…ary (H3, M1) H3 — `Cookie` and the `headers: Vec<(String, String)>` slot on `ChromiumOptions` are both `pub`, so a downstream consumer can build entries without going through `Cookie::parse_kv` or `parse_header_kv`. The previous `install_cookies` / `install_extra_headers` only ran `validate_no_ctl` (CR/LF/NUL only) on the values — `:` in a header name and `=` / whitespace in a cookie name slipped through. Factor `validate_header_name`, `validate_cookie_name`, and `validate_cookie_value` into free functions and call them from both the parser path (`parse_header_kv`, `Cookie::parse_kv`) and the install path. Cookie domain/path also flow through `validate_no_ctl` when present. M1 — extend `validate_no_ctl` to reject every C0 control byte (`< 0x20`) plus DEL (`0x7F`) instead of only CR/LF/NUL. Tab is included in the rejection set; HTTP whitespace folding is deprecated (RFC 7230 §3.2.4) and Plumb has no compatibility need for it. Adds five unit tests exercising the validators directly so the H3 library boundary is regression-tested. Addresses 06-security-auditor findings H3 and M1 on PR #215.
Replace the hand-rolled `origin_of` extractor with `url::Url::parse(s).origin().ascii_serialization()`. The WHATWG origin serialization handles default-port elision (`:443` → ``, `:80` → ``), scheme case-folding, IDNA host normalization, and stripping of userinfo / path / query / fragment — so a target URL like `https://Example.com:443/path?q=1#frag` correctly matches a Playwright storage-state origin recorded as `https://example.com`. Opaque origins (`data:`, `file:`) return `None` and cannot match any recorded site origin, which is the right semantics for `localStorage` isolation. `url v2` is already a transitive dep via `chromiumoxide`; promote it to a direct workspace dependency to make the addition explicit. Adds four `origin_of` tests covering default ports, scheme/host case, userinfo / query / fragment stripping, and opaque origins. Addresses 06-security-auditor finding M2 on PR #215.
Extend the storage-state parser's cookie sweep to call `validate_no_ctl` on `domain` and `path` in addition to `name` and `value`. Both flow into a `Network.setCookies` CDP call alongside name/value, so a CR/LF in either smuggles just as effectively as one in the value. Playwright files are typically machine-written, but Plumb cannot trust their provenance. Also moves the validation + sort into `parse_str` (with `load_from_path` delegating) so the sanitization is exercisable from unit tests without disk I/O. Adds three tests that drive `parse_str` directly with CR/LF in domain, LF in path, and ESC (`\\u001b`, a non-CRLF C0 byte) in value to lock in both the M1 (full C0 + DEL) and M3 (domain/path) coverage. Addresses 06-security-auditor finding M3 on PR #215.
…nd --storage-state (M4)
The FakeDriver path never reaches the cdp-side safe-path check, so a
test that runs `plumb lint plumb-fake://hello` with a malicious
absolute path would silently accept it before this commit.
Promote the safe-path check to public API as
`plumb_cdp::validate_safe_path` and call it from `plumb-cli`'s lint
orchestrator before driver dispatch. The check now fires regardless
of the URL scheme.
Add two CLI integration tests:
- `lint_rejects_auth_script_outside_cwd` — writes a benign `.js` in
one tempdir and runs the CLI from a different tempdir, expecting
exit code 2 with `stderr.contains("outside the current working
directory")`.
- `lint_rejects_storage_state_outside_cwd` — same shape with
`--storage-state`.
Addresses 06-security-auditor finding M4 on PR #215.
Pure whitespace from `cargo fmt` on the diff introduced by the H1 / M3 commits — separate from the behavior changes for clean review.
|
Addresses 06-security-auditor REQUEST_CHANGES findings on PR #215.
Notes
Local validation
|
rustdoc lint private_intra_doc_links (denied by -D warnings) blocked the docs build because canonicalize_safe_path is private. Switch the doc references to plain code spans; they still read fine in rustdoc and are no longer broken links.
Closes #74, #75, #76, #77.
Summary
Bundles four CDP-side V1 driver-ergonomics features that all extend the same
TargetandChromiumOptionstypes inplumb-cdp. Splitting would have forced three rebases on the same struct.--wait-for <selector>and--wait-ms <ms>— wait for an element to render or sleep an extra interval before the snapshot.--cookie name=value,--header 'name: value',--auth-script <path>— pre-inject session state and run a.jsbootstrap before navigation. Cookies and headers are control-character-validated at parse time; auth-script paths are canonicalized and refused when they resolve outside CWD.--storage-state <path>— load a Playwrightstorage-state.json. Cookies install before navigation; localStorage writes viapage.evaluateafter navigation when the origin matches. Parser usesserde(deny_unknown_fields).--disable-animations [bool],--hide-scrollbars [bool],--dpr <factor>— three per-target capture knobs. The first two default totrueand inject CSS viaPage.addScriptToEvaluateOnNewDocument.--dproverrides the viewport'sdevice_pixel_ratiothroughEmulation.setDeviceMetricsOverride.A new
pre_navigatehelper centralizes the "set up the page before goto" stages — animation killer, scrollbar killer, auth script, headers, cookies, storage-state cookies — soChromiumDriverandPersistentBrowsershare one implementation.CLI flags wire through the new
LintArgsstruct inplumb_cli::commands::lint.Determinism guarantees
(name, value)before CDP traffic.(domain, name)and origins byorigin.tokio::time::timeout(noInstant::now).just determinism-checkproduce byte-identical JSON.Security guarantees
--cookieand--headerreject CR / LF / NUL bytes (header injection).:.--auth-scriptand--storage-statepaths are canonicalized; resolution outside CWD is refused..js.Test plan
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo test --workspace --all-features(all suites pass; 12 new lib unit tests inplumb-cdp, 8 new integration tests inplumb-cli)just determinism-check(3 runs byte-identical)cargo deny check(advisories ok, bans ok, licenses ok, sources ok)just validate(full local mirror of CI passes)Files changed
crates/plumb-cdp/src/lib.rs—Target+ChromiumOptionsextension,Cookie,parse_header_kv,StorageState{,Cookie,Origin,LocalStorageEntry},pre_navigate,apply_post_navigate_waits,apply_storage_state_local_storage,inject_scrollbar_killer,inject_auth_script,install_extra_headers,install_cookies,install_storage_state_cookies,wait_for_selector,canonicalize_safe_path. New variants onCdpError. 12 new unit tests.crates/plumb-cdp/Cargo.toml— addsserde+serde_jsonto runtime deps.crates/plumb-cli/src/main.rs— 8 newLintflags.crates/plumb-cli/src/commands/lint.rs— newLintArgsstruct, capture-knob threading, cookie/header parsing, ChromiumOptions wiring.crates/plumb-cli/tests/cli_integration.rs— 8 new integration tests.crates/plumb-mcp/src/lib.rs,crates/plumb-cdp/tests/*.rs,crates/plumb-cdp/benches/cdp_benchmarks.rs— call-site updates for..Target::default()/..ChromiumOptions::default()after the field expansion.docs/src/cli.md— updated flag table.🤖 Generated with Claude Code