From 56a2dc372a601862462500a938eb0dcf4ae111d7 Mon Sep 17 00:00:00 2001 From: jasonwitty Date: Mon, 24 Aug 2026 09:46:20 -0700 Subject: [PATCH] Reject unknown options in parse_args instead of treating them as the URL An older socktop handed a newer flag (webterm 0.3.9's restricted shell passing --no-kill to 1.60.1) silently parsed the flag as the positional websocket URL and offered to overwrite the named profile's URL with the literal flag text. Unknown options now fail with 'Unknown option' and exit code 2 (help remains exit 0), with a regression test covering the exact incident shape. Co-Authored-By: Claude Fable 5 --- socktop/src/main.rs | 14 ++++++++++++++ socktop/tests/cli_args.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/socktop/src/main.rs b/socktop/src/main.rs index 746c0c9..2c7991a 100644 --- a/socktop/src/main.rs +++ b/socktop/src/main.rs @@ -124,6 +124,15 @@ pub(crate) fn parse_args>(args: I) -> Result { + // An unrecognized option must never fall through to the + // positional URL slot: an older binary handed a newer flag + // would otherwise "connect" to the flag text — and offer to + // save it over a named profile's URL. + if arg.starts_with('-') { + return Err(format!( + "Unknown option '{arg}'. Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--verify-hostname] [--profile NAME|-P NAME] [--save] [--demo] [--compact] [--no-kill] [ws://HOST:PORT/ws]" + )); + } if url.is_none() { url = Some(arg); } else { @@ -155,6 +164,11 @@ async fn main() -> Result<(), Box> { Ok(v) => v, Err(msg) => { eprintln!("{msg}"); + // --help produces the bare usage text and exits cleanly; real + // parse errors must be visible to scripts and CI as a failure. + if !msg.starts_with("Usage:") { + std::process::exit(2); + } return Ok(()); } }; diff --git a/socktop/tests/cli_args.rs b/socktop/tests/cli_args.rs index 6118a69..defcdce 100644 --- a/socktop/tests/cli_args.rs +++ b/socktop/tests/cli_args.rs @@ -156,3 +156,29 @@ fn test_no_kill_env_var_accepted() { String::from_utf8_lossy(&out.stderr) ); } + +#[test] +fn test_unknown_option_rejected_not_treated_as_url() { + // Regression guard for the socktop.io incident (Aug 2026): socktop 1.60.1 + // parsed the then-unknown --no-kill flag as the positional websocket URL, + // which made it prompt to overwrite the 'local' profile's URL with the + // literal string "--no-kill". Unknown options must fail loudly instead of + // falling through to the URL slot. + let exe = env!("CARGO_BIN_EXE_socktop"); + let out = Command::new(exe) + .args(["--not-a-real-flag", "--dry-run", "ws://127.0.0.1:3000/ws"]) + .output() + .expect("run socktop with unknown flag"); + assert_eq!( + out.status.code(), + Some(2), + "unknown option should exit 2, got: {:?}\nstderr: {}", + out.status.code(), + String::from_utf8_lossy(&out.stderr) + ); + let err = String::from_utf8_lossy(&out.stderr); + assert!( + err.contains("Unknown option '--not-a-real-flag'"), + "stderr should name the rejected option\n{err}" + ); +}