From f4eee1408d798c937cb1843a8c601fb13b170e5d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:45:03 +0000 Subject: [PATCH] Fix command injection vulnerability in stdlib process.spawn Co-authored-by: Tcode-Motion <188012755+Tcode-Motion@users.noreply.github.com> --- Cargo.lock | 1 + stdlib/Cargo.toml | 1 + stdlib/src/process.rs | 21 ++++++++++++++++-- stdlib/tests/stdlib_tests.rs | 41 +++++++++++++++++++++++++++++++++++- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 380393ee..c04029ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3868,6 +3868,7 @@ dependencies = [ "serde_json", "sha-1", "sha2 0.10.9", + "shlex", "tar", "techscript_runtime", "tiny_http", diff --git a/stdlib/Cargo.toml b/stdlib/Cargo.toml index 807e727c..9a7a0395 100644 --- a/stdlib/Cargo.toml +++ b/stdlib/Cargo.toml @@ -36,3 +36,4 @@ uuid = { version = "1", features = ["v4"] } rustls = { version = "0.23", optional = true } tokio = { version = "1", features = ["rt", "macros", "sync", "time"], optional = true } hex = "0.4.3" +shlex = "2.0.1" diff --git a/stdlib/src/process.rs b/stdlib/src/process.rs index fb9a27fc..4626dac6 100644 --- a/stdlib/src/process.rs +++ b/stdlib/src/process.rs @@ -114,8 +114,25 @@ impl StdlibRegistry { )) } }; - Command::new("cmd") - .args(["/C", &cmd]) + + let parsed = shlex::split(&cmd).ok_or_else(|| { + RuntimeError::new( + RuntimeErrorKind::InvalidOperation("Failed to parse command string".to_string()), + None, + None, + ) + })?; + + if parsed.is_empty() { + return Err(RuntimeError::new( + RuntimeErrorKind::InvalidOperation("Empty command string".to_string()), + None, + None, + )); + } + + Command::new(&parsed[0]) + .args(&parsed[1..]) .spawn() .map_err(|e| { RuntimeError::new( diff --git a/stdlib/tests/stdlib_tests.rs b/stdlib/tests/stdlib_tests.rs index 7d530343..553fd782 100644 --- a/stdlib/tests/stdlib_tests.rs +++ b/stdlib/tests/stdlib_tests.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::collections::HashSet; use std::rc::Rc; -use techscript_runtime::{context::Capability, value::RuntimeValue, RuntimeConfig, RuntimeContext}; +use techscript_runtime::{context::Capability, value::RuntimeValue, RuntimeConfig, RuntimeContext, error::RuntimeErrorKind}; use techscript_stdlib::StdlibRegistry; #[test] @@ -1134,3 +1134,42 @@ fn test_ai_generate_text() { let val = res.unwrap(); assert!(val.as_string().unwrap().contains("Prompt: What is 2+2?")); } + +#[test] +fn test_process_spawn_parsing() { + let registry = StdlibRegistry::new(); + let process_module = registry.get_module("std.process").unwrap(); + + let mut config = RuntimeConfig::default(); + config.capabilities.insert(Capability::Process); + let mut ctx = RuntimeContext::new(config); + + let spawn_fn = process_module.exports.get("spawn").unwrap(); + + // Valid command + let res = spawn_fn.call( + &mut ctx, + vec![RuntimeValue::Str("echo 'hello world'".to_string())], + ); + assert!(res.is_ok()); + + // Empty command + let res_empty = spawn_fn.call(&mut ctx, vec![RuntimeValue::Str(" ".to_string())]); + assert!(res_empty.is_err()); + let err = res_empty.unwrap_err(); + if let RuntimeErrorKind::InvalidOperation(msg) = err.kind.clone() { + assert_eq!(msg, "Empty command string"); + } else { + panic!("Expected InvalidOperation error"); + } + + // Invalid shell string (missing closing quote) + let res_invalid = spawn_fn.call(&mut ctx, vec![RuntimeValue::Str("echo 'hello".to_string())]); + assert!(res_invalid.is_err()); + let err = res_invalid.unwrap_err(); + if let RuntimeErrorKind::InvalidOperation(msg) = err.kind.clone() { + assert_eq!(msg, "Failed to parse command string"); + } else { + panic!("Expected InvalidOperation error"); + } +}