From 250cb67ca8e6ff231c1ed9de7dc004d0a5985e6d Mon Sep 17 00:00:00 2001 From: Nitjsefnie Date: Thu, 22 Oct 2026 21:08:12 +0200 Subject: [PATCH 1/3] Preserve OAuth URLs when launching Windows browsers Bypass the command interpreter for Grok and Cursor browser launches. Report Grok spawn errors and cover adversarial query parameters in the production Windows argument builders. Co-Authored-By: GPT-6.1 Sol --- src/providers/cursor/auth.rs | 14 +++++-- src/providers/grok/auth/login.rs | 23 ++++++++--- tests/support/oauth_browser_launch.rs | 58 +++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 tests/support/oauth_browser_launch.rs diff --git a/src/providers/cursor/auth.rs b/src/providers/cursor/auth.rs index bcbbdc5b..efc4718f 100644 --- a/src/providers/cursor/auth.rs +++ b/src/providers/cursor/auth.rs @@ -341,9 +341,8 @@ fn open_cursor_login_url(url: &str) -> anyhow::Result<()> { let status = if cfg!(target_os = "macos") { std::process::Command::new("open").arg(url).status()? } else if cfg!(target_os = "windows") { - std::process::Command::new("cmd") - .args(["/c", "start", "", url]) - .status()? + let (program, args) = windows_browser_command(url); + std::process::Command::new(program).args(args).status()? } else { std::process::Command::new("xdg-open").arg(url).status()? }; @@ -353,6 +352,15 @@ fn open_cursor_login_url(url: &str) -> anyhow::Result<()> { Ok(()) } +fn windows_browser_command(url: &str) -> (&str, Vec<&str>) { + // Keep URL metacharacters out of a command interpreter. + ("rundll32.exe", vec!["url.dll,FileProtocolHandler", url]) +} + +#[cfg(test)] +#[path = "../../../tests/support/oauth_browser_launch.rs"] +mod browser_launch_tests; + fn random_base64_url(len: usize) -> String { let mut bytes = vec![0u8; len]; rand::thread_rng().fill_bytes(&mut bytes); diff --git a/src/providers/grok/auth/login.rs b/src/providers/grok/auth/login.rs index b7b7e74c..45577822 100644 --- a/src/providers/grok/auth/login.rs +++ b/src/providers/grok/auth/login.rs @@ -47,7 +47,9 @@ pub fn login>(store: &GrokTokenStore) -> anyhow::R let auth_url = authorize_url(&discovery, &redirect_uri, &pkce, &state)?; println!("Open this URL in your browser to authorize:\n\n {auth_url}\n"); - open_browser(&auth_url); + if let Err(err) = open_browser(&auth_url) { + println!("Could not open browser automatically: {err}"); + } let code = wait_for_callback(&listener, &state, LOGIN_TIMEOUT)?; let tokens = exchange_code(&client, &discovery, &code, &pkce, &redirect_uri)?; validate_tokens(&tokens)?; @@ -280,21 +282,32 @@ fn respond(stream: &mut TcpStream, status: &str, body: &str) { let _ = stream.write_all(response.as_bytes()); } -fn open_browser(url: &str) { +fn open_browser(url: &str) -> anyhow::Result<()> { #[cfg(target_os = "macos")] let command = ("open", vec![url]); #[cfg(target_os = "linux")] let command = ("xdg-open", vec![url]); #[cfg(target_os = "windows")] - let command = ("cmd", vec!["/C", "start", "", url]); + let command = windows_browser_command(url); #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] { - let _ = std::process::Command::new(command.0) + std::process::Command::new(command.0) .args(command.1) - .spawn(); + .spawn()?; } + Ok(()) +} + +#[cfg(any(windows, test))] +fn windows_browser_command(url: &str) -> (&str, Vec<&str>) { + // Keep URL metacharacters out of a command interpreter. + ("rundll32.exe", vec!["url.dll,FileProtocolHandler", url]) } +#[cfg(test)] +#[path = "../../../../tests/support/oauth_browser_launch.rs"] +mod browser_launch_tests; + fn now_ms() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) diff --git a/tests/support/oauth_browser_launch.rs b/tests/support/oauth_browser_launch.rs new file mode 100644 index 00000000..02958157 --- /dev/null +++ b/tests/support/oauth_browser_launch.rs @@ -0,0 +1,58 @@ +use super::windows_browser_command; + +const LOGIN_URLS: [&str; 2] = [ + "https://auth.x.ai/oauth2/authorize?response_type=code&client_id=fixture-client&redirect_uri=http%3A%2F%2F127.0.0.1%3A12345%2Fcallback&scope=openid+profile&code_challenge=ab^cd|ef(gh)&code_challenge_method=S256&state=a%26b", + "https://cursor.com/loginDeepControl?challenge=ab^cd|ef(gh)&uuid=12345678-abcd-4321-abcd-123456789abc&mode=login&redirectTarget=cli%3A%2F%2Fcallback", +]; + +#[test] +fn oauth_browser_launch_keeps_urls_out_of_cmd() { + for url in LOGIN_URLS { + let (program, args) = windows_browser_command(url); + assert_eq!(program, "rundll32.exe", "OAuth URLs must bypass cmd"); + assert_eq!(args, ["url.dll,FileProtocolHandler", url]); + } +} + +#[cfg(windows)] +#[test] +fn oauth_browser_launch_roundtrips_through_native_process() { + // Substitute a native argv recorder for rundll32 so no browser is opened. + let temp = tempfile::tempdir().unwrap(); + let source = temp.path().join("argv.rs"); + let recorder = temp.path().join("argv.exe"); + std::fs::write( + &source, + r#"fn main() { + for arg in std::env::args().skip(1) { + println!("{arg}"); + } + }"#, + ) + .unwrap(); + let compile = std::process::Command::new("rustc") + .arg(&source) + .arg("-o") + .arg(&recorder) + .output() + .unwrap(); + assert!( + compile.status.success(), + "argv recorder did not compile: {}", + String::from_utf8_lossy(&compile.stderr) + ); + for url in LOGIN_URLS { + let (program, args) = windows_browser_command(url); + assert_eq!(program, "rundll32.exe", "OAuth URLs must bypass cmd"); + let output = std::process::Command::new(&recorder) + .args(args) + .output() + .unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert_eq!( + stdout.lines().collect::>(), + ["url.dll,FileProtocolHandler", url] + ); + } +} From c9ba219cba45839c000cb688e61b4faa3bc3394a Mon Sep 17 00:00:00 2001 From: Nitjsefnie Date: Thu, 22 Oct 2026 21:26:16 +0200 Subject: [PATCH 2/3] Exercise production OAuth browser launch paths Inject platform selection and process execution into the private launchers so regression tests cover their actual Windows branches. Keep native recorder coverage on those paths and verify unchanged Unix arguments and launch error propagation. Co-Authored-By: GPT-6.1 Sol --- src/providers/cursor/auth.rs | 27 +++++++---- src/providers/grok/auth/login.rs | 35 ++++++++------ tests/support/oauth_browser_launch.rs | 69 ++++++++++++++++++++------- 3 files changed, 92 insertions(+), 39 deletions(-) diff --git a/src/providers/cursor/auth.rs b/src/providers/cursor/auth.rs index efc4718f..555fa888 100644 --- a/src/providers/cursor/auth.rs +++ b/src/providers/cursor/auth.rs @@ -196,7 +196,11 @@ pub fn run_cursor_login() -> anyhow::Result> { println!("Open this URL to authenticate with Cursor:"); println!("{}", login.login_url); println!(); - if let Err(err) = open_cursor_login_url(&login.login_url) { + if let Err(err) = open_cursor_login_url( + &login.login_url, + std::env::consts::OS, + std::process::Command::status, + ) { println!("Could not open browser automatically: {err}"); } println!("Waiting for Cursor login..."); @@ -337,15 +341,19 @@ fn parse_jwt_claims(token: &str) -> Option { serde_json::from_slice(&decoded).ok() } -fn open_cursor_login_url(url: &str) -> anyhow::Result<()> { - let status = if cfg!(target_os = "macos") { - std::process::Command::new("open").arg(url).status()? - } else if cfg!(target_os = "windows") { - let (program, args) = windows_browser_command(url); - std::process::Command::new(program).args(args).status()? +fn open_cursor_login_url( + url: &str, + platform: &str, + execute: impl FnOnce(&mut std::process::Command) -> std::io::Result, +) -> anyhow::Result<()> { + let command = if platform == "macos" { + ("open", vec![url]) + } else if platform == "windows" { + windows_browser_command(url) } else { - std::process::Command::new("xdg-open").arg(url).status()? + ("xdg-open", vec![url]) }; + let status = execute(std::process::Command::new(command.0).args(command.1))?; if !status.success() { anyhow::bail!("open command exited with {status}"); } @@ -357,6 +365,9 @@ fn windows_browser_command(url: &str) -> (&str, Vec<&str>) { ("rundll32.exe", vec!["url.dll,FileProtocolHandler", url]) } +#[cfg(test)] +use open_cursor_login_url as open_login_url; + #[cfg(test)] #[path = "../../../tests/support/oauth_browser_launch.rs"] mod browser_launch_tests; diff --git a/src/providers/grok/auth/login.rs b/src/providers/grok/auth/login.rs index 45577822..4b99c526 100644 --- a/src/providers/grok/auth/login.rs +++ b/src/providers/grok/auth/login.rs @@ -47,7 +47,11 @@ pub fn login>(store: &GrokTokenStore) -> anyhow::R let auth_url = authorize_url(&discovery, &redirect_uri, &pkce, &state)?; println!("Open this URL in your browser to authorize:\n\n {auth_url}\n"); - if let Err(err) = open_browser(&auth_url) { + if let Err(err) = open_browser( + &auth_url, + std::env::consts::OS, + std::process::Command::spawn, + ) { println!("Could not open browser automatically: {err}"); } let code = wait_for_callback(&listener, &state, LOGIN_TIMEOUT)?; @@ -282,28 +286,29 @@ fn respond(stream: &mut TcpStream, status: &str, body: &str) { let _ = stream.write_all(response.as_bytes()); } -fn open_browser(url: &str) -> anyhow::Result<()> { - #[cfg(target_os = "macos")] - let command = ("open", vec![url]); - #[cfg(target_os = "linux")] - let command = ("xdg-open", vec![url]); - #[cfg(target_os = "windows")] - let command = windows_browser_command(url); - #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] - { - std::process::Command::new(command.0) - .args(command.1) - .spawn()?; - } +fn open_browser( + url: &str, + platform: &str, + execute: impl FnOnce(&mut std::process::Command) -> std::io::Result, +) -> anyhow::Result<()> { + let command = match platform { + "macos" => ("open", vec![url]), + "linux" => ("xdg-open", vec![url]), + "windows" => windows_browser_command(url), + _ => return Ok(()), + }; + execute(std::process::Command::new(command.0).args(command.1))?; Ok(()) } -#[cfg(any(windows, test))] fn windows_browser_command(url: &str) -> (&str, Vec<&str>) { // Keep URL metacharacters out of a command interpreter. ("rundll32.exe", vec!["url.dll,FileProtocolHandler", url]) } +#[cfg(test)] +use open_browser as open_login_url; + #[cfg(test)] #[path = "../../../../tests/support/oauth_browser_launch.rs"] mod browser_launch_tests; diff --git a/tests/support/oauth_browser_launch.rs b/tests/support/oauth_browser_launch.rs index 02958157..f90a3c4f 100644 --- a/tests/support/oauth_browser_launch.rs +++ b/tests/support/oauth_browser_launch.rs @@ -1,4 +1,4 @@ -use super::windows_browser_command; +use super::open_login_url; const LOGIN_URLS: [&str; 2] = [ "https://auth.x.ai/oauth2/authorize?response_type=code&client_id=fixture-client&redirect_uri=http%3A%2F%2F127.0.0.1%3A12345%2Fcallback&scope=openid+profile&code_challenge=ab^cd|ef(gh)&code_challenge_method=S256&state=a%26b", @@ -8,9 +8,35 @@ const LOGIN_URLS: [&str; 2] = [ #[test] fn oauth_browser_launch_keeps_urls_out_of_cmd() { for url in LOGIN_URLS { - let (program, args) = windows_browser_command(url); - assert_eq!(program, "rundll32.exe", "OAuth URLs must bypass cmd"); - assert_eq!(args, ["url.dll,FileProtocolHandler", url]); + let error = open_login_url(url, "windows", |command| { + assert_eq!( + command.get_program(), + "rundll32.exe", + "OAuth URLs must bypass cmd" + ); + assert_eq!( + command.get_args().collect::>(), + ["url.dll,FileProtocolHandler", url] + ); + Err(std::io::Error::other("browser launch intercepted")) + }) + .unwrap_err(); + assert_eq!(error.to_string(), "browser launch intercepted"); + } +} + +#[test] +fn oauth_browser_launch_preserves_unix_arguments() { + for (platform, program) in [("macos", "open"), ("linux", "xdg-open")] { + for url in LOGIN_URLS { + let error = open_login_url(url, platform, |command| { + assert_eq!(command.get_program(), program); + assert_eq!(command.get_args().collect::>(), [url]); + Err(std::io::Error::other("browser launch intercepted")) + }) + .unwrap_err(); + assert_eq!(error.to_string(), "browser launch intercepted"); + } } } @@ -42,17 +68,28 @@ fn oauth_browser_launch_roundtrips_through_native_process() { String::from_utf8_lossy(&compile.stderr) ); for url in LOGIN_URLS { - let (program, args) = windows_browser_command(url); - assert_eq!(program, "rundll32.exe", "OAuth URLs must bypass cmd"); - let output = std::process::Command::new(&recorder) - .args(args) - .output() - .unwrap(); - assert!(output.status.success()); - let stdout = String::from_utf8(output.stdout).unwrap(); - assert_eq!( - stdout.lines().collect::>(), - ["url.dll,FileProtocolHandler", url] - ); + let error = open_login_url(url, "windows", |command| { + assert_eq!( + command.get_program(), + "rundll32.exe", + "OAuth URLs must bypass cmd" + ); + assert_eq!( + command.get_args().collect::>(), + ["url.dll,FileProtocolHandler", url] + ); + let output = std::process::Command::new(&recorder) + .args(command.get_args()) + .output()?; + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert_eq!( + stdout.lines().collect::>(), + ["url.dll,FileProtocolHandler", url] + ); + Err(std::io::Error::other("browser launch intercepted")) + }) + .unwrap_err(); + assert_eq!(error.to_string(), "browser launch intercepted"); } } From 21a4431c340b7015e056c04d2638662d8b08aca7 Mon Sep 17 00:00:00 2001 From: Nitjsefnie Date: Thu, 22 Oct 2026 21:50:21 +0200 Subject: [PATCH 3/3] Allow intentional reuse of OAuth launcher test modules Scope the duplicate module lint allowance to the shared test includes, which exercise each provider's private launcher. Co-Authored-By: GPT-6.1 Sol --- src/providers/cursor/auth.rs | 2 ++ src/providers/grok/auth/login.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/providers/cursor/auth.rs b/src/providers/cursor/auth.rs index 555fa888..8ec56ea9 100644 --- a/src/providers/cursor/auth.rs +++ b/src/providers/cursor/auth.rs @@ -368,7 +368,9 @@ fn windows_browser_command(url: &str) -> (&str, Vec<&str>) { #[cfg(test)] use open_cursor_login_url as open_login_url; +// Run the shared assertions against each provider's private launcher. #[cfg(test)] +#[allow(clippy::duplicate_mod)] #[path = "../../../tests/support/oauth_browser_launch.rs"] mod browser_launch_tests; diff --git a/src/providers/grok/auth/login.rs b/src/providers/grok/auth/login.rs index 4b99c526..a8efe5e6 100644 --- a/src/providers/grok/auth/login.rs +++ b/src/providers/grok/auth/login.rs @@ -309,7 +309,9 @@ fn windows_browser_command(url: &str) -> (&str, Vec<&str>) { #[cfg(test)] use open_browser as open_login_url; +// Run the shared assertions against each provider's private launcher. #[cfg(test)] +#[allow(clippy::duplicate_mod)] #[path = "../../../../tests/support/oauth_browser_launch.rs"] mod browser_launch_tests;