From f9b33ac23d45527660eaec9030c9367307d2460a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 16 Feb 2026 23:55:40 -0600 Subject: [PATCH] fix: reject invalid --headers JSON, empty frame commands, and --cdp + --extension combo (#488) ## Summary - Return a `ParseError` when `--headers` receives invalid JSON instead of silently dropping the headers and proceeding - Reject `frame` commands that provide no `selector`, `name`, or `url` (previously returned `{ switched: true }` without doing anything) - Add missing mutual exclusion check for `--cdp` + `--extension` (extensions require a local browser, not a CDP connection) --- cli/src/commands.rs | 18 ++++++++++++------ cli/src/main.rs | 10 ++++++++++ src/protocol.test.ts | 15 +++++++++++++++ src/protocol.ts | 8 ++++++++ 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/cli/src/commands.rs b/cli/src/commands.rs index efdfd97..abd4ef8 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -103,9 +103,12 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result(headers_json) { - nav_cmd["headers"] = headers; - } + let headers = serde_json::from_str::(headers_json) + .map_err(|_| ParseError::InvalidValue { + message: format!("Invalid JSON for --headers: {}", headers_json), + usage: "open --headers '{\"Key\": \"Value\"}'", + })?; + nav_cmd["headers"] = headers; } // Include iOS device info if specified (needed for auto-launch with existing daemon) if flags.provider.as_deref() == Some("ios") { @@ -1830,9 +1833,12 @@ mod tests { fn test_navigate_with_invalid_headers_json() { let mut flags = default_flags(); flags.headers = Some("not valid json".to_string()); - let cmd = parse_command(&args("open api.example.com"), &flags).unwrap(); - // Invalid JSON should result in no headers field (graceful handling) - assert!(cmd.get("headers").is_none()); + let result = parse_command(&args("open api.example.com"), &flags); + // Invalid JSON should return a ParseError, not silently drop headers + assert!(result.is_err()); + let err = result.unwrap_err(); + let msg = err.format(); + assert!(msg.contains("Invalid JSON for --headers")); } // === Set Headers Tests === diff --git a/cli/src/main.rs b/cli/src/main.rs index c5c1d51..185a922 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -336,6 +336,16 @@ fn main() { exit(1); } + if flags.cdp.is_some() && !flags.extensions.is_empty() { + let msg = "Cannot use --extension with --cdp (extensions require local browser)"; + if flags.json { + println!(r#"{{"success":false,"error":"{}"}}"#, msg); + } else { + eprintln!("{} {}", color::error_indicator(), msg); + } + exit(1); + } + // Auto-connect to existing browser if flags.auto_connect { let mut launch_cmd = json!({ diff --git a/src/protocol.test.ts b/src/protocol.test.ts index a652066..0e14263 100644 --- a/src/protocol.test.ts +++ b/src/protocol.test.ts @@ -733,6 +733,21 @@ describe('parseCommand', () => { expect(result.success).toBe(true); }); + it('should reject frame with no selector, name, or url', () => { + const result = parseCommand(cmd({ id: '1', action: 'frame' })); + expect(result.success).toBe(false); + }); + + it('should parse frame with name', () => { + const result = parseCommand(cmd({ id: '1', action: 'frame', name: 'myframe' })); + expect(result.success).toBe(true); + }); + + it('should parse frame with url', () => { + const result = parseCommand(cmd({ id: '1', action: 'frame', url: 'https://example.com' })); + expect(result.success).toBe(true); + }); + it('should parse mainframe', () => { const result = parseCommand(cmd({ id: '1', action: 'mainframe' })); expect(result.success).toBe(true); diff --git a/src/protocol.ts b/src/protocol.ts index 62bc9df..72e00b4 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -1001,6 +1001,14 @@ export function parseCommand(input: string): ParseResult { return { success: false, error: 'Either content or url must be provided', id }; } + if (command.action === 'frame' && !command.selector && !command.name && !command.url) { + return { + success: false, + error: 'frame command requires at least one of: selector, name, or url', + id, + }; + } + return { success: true, command }; }