fix(cli): close --on-change command injection via sh -c (P0) (#20)

* fix(cli): close --on-change command injection via sh -c (P0)

The --on-change flag on `webclaw watch` (single-URL, line 1588) and
`webclaw watch` multi-URL mode (line 1738) previously handed the entire
user-supplied string to `tokio::process::Command::new("sh").arg("-c").arg(cmd)`.
Any path that can influence that string — a malicious config file, an MCP
client driven by an LLM with prompt-injection exposure, an untrusted
environment variable substitution — gets arbitrary shell execution.

The command is now tokenized with `shlex::split` (POSIX-ish quoting rules)
and executed directly via `Command::new(prog).args(args)`. Metacharacters
like `;`, `&&`, `|`, `$()`, `<(...)`, env expansion, and globbing no longer
fire.

An explicit opt-in escape hatch is available for users who genuinely need
a shell pipeline: `WEBCLAW_ALLOW_SHELL=1` preserves the old `sh -c` path
and logs a warning on every invocation so it can't slip in silently.

Both call sites now route through a shared `spawn_on_change()` helper.

Adds `shlex = "1"` to webclaw-cli dependencies.

Version: 0.3.13 -> 0.3.14
CHANGELOG updated.

Surfaced by the 2026-04-16 workspace audit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(brand): fix clippy 1.95 unnecessary_sort_by errors

Pre-existing sort_by calls in brand.rs became hard errors under clippy
1.95. Switch to sort_by_key with std::cmp::Reverse. Pure refactor — same
ordering, no behavior change. Bundled here so CI goes green on the P0
command-injection fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Valerio 2026-04-16 18:37:02 +02:00 committed by GitHub
parent 6316b1a6e7
commit 1352f48e05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 73 additions and 39 deletions

13
Cargo.lock generated
View file

@ -3102,7 +3102,7 @@ dependencies = [
[[package]]
name = "webclaw-cli"
version = "0.3.13"
version = "0.3.14"
dependencies = [
"clap",
"dotenvy",
@ -3110,6 +3110,7 @@ dependencies = [
"regex",
"reqwest",
"serde_json",
"shlex",
"tokio",
"tracing",
"tracing-subscriber",
@ -3122,7 +3123,7 @@ dependencies = [
[[package]]
name = "webclaw-core"
version = "0.3.13"
version = "0.3.14"
dependencies = [
"ego-tree",
"once_cell",
@ -3140,7 +3141,7 @@ dependencies = [
[[package]]
name = "webclaw-fetch"
version = "0.3.13"
version = "0.3.14"
dependencies = [
"bytes",
"calamine",
@ -3162,7 +3163,7 @@ dependencies = [
[[package]]
name = "webclaw-llm"
version = "0.3.13"
version = "0.3.14"
dependencies = [
"async-trait",
"reqwest",
@ -3175,7 +3176,7 @@ dependencies = [
[[package]]
name = "webclaw-mcp"
version = "0.3.13"
version = "0.3.14"
dependencies = [
"dirs",
"dotenvy",
@ -3196,7 +3197,7 @@ dependencies = [
[[package]]
name = "webclaw-pdf"
version = "0.3.13"
version = "0.3.14"
dependencies = [
"pdf-extract",
"thiserror",