anodizer_core/user_command.rs
1//! Spawn a user-supplied command (e.g. `publisher.cmd`) with a clean,
2//! whitelisted environment.
3//!
4//! Centralised here so the `Command::new(<arbitrary>)` shell-out lives
5//! inside the module-boundaries allow-list. Inlining this in the CLI
6//! crate would put `Command::new` outside the allow-list and counts
7//! as a boundary violation.
8
9use std::ffi::OsStr;
10use std::process::Command;
11
12use anyhow::Result;
13
14/// Environment variables that are inherited from the parent process
15/// when constructing a sandboxed `Command`. Anything else must be
16/// explicitly added via `Command::env`.
17///
18/// This whitelist exists to prevent accidental leakage of release
19/// credentials (`GITHUB_TOKEN`, `COSIGN_*`, signing keys, etc.) into
20/// arbitrary user-supplied commands.
21pub const ENV_WHITELIST: &[&str] = &[
22 "HOME",
23 "USER",
24 "USERPROFILE",
25 "TMPDIR",
26 "TMP",
27 "TEMP",
28 "PATH",
29 "SYSTEMROOT",
30];
31
32/// Construct a `Command` whose argv is `argv` and whose environment is
33/// reset to the [`ENV_WHITELIST`] subset of the parent's env. The first
34/// element of `argv` is the program; the rest are arguments. The caller
35/// is responsible for adding any further env vars / cwd / I/O config
36/// before invoking `output()`.
37///
38/// Returns `Err` when `argv` is empty — surfacing a clear error at the
39/// allow-listed boundary is preferable to deferring failure to the
40/// kernel via an empty `program` path.
41pub fn whitelisted<S: AsRef<OsStr>>(argv: &[S]) -> Result<Command> {
42 anyhow::ensure!(!argv.is_empty(), "user command argv cannot be empty");
43 let program = argv[0].as_ref();
44 let mut cmd = Command::new(program);
45 if argv.len() > 1 {
46 cmd.args(&argv[1..]);
47 }
48 cmd.env_clear();
49 for key in ENV_WHITELIST {
50 if let Ok(val) = std::env::var(key) {
51 cmd.env(key, val);
52 }
53 }
54 Ok(cmd)
55}