Filter environment variables in both sandbox modes
Whitelist mode now clears the parent env and re-adds a small allowlist (identity, terminal, locale, proxy, non-GUI XDG, vendor prefixes). Blacklist mode strips cloud credentials, backup passphrases, dangling socket pointers, and anything matching *_TOKEN, *_SECRET, *_PASSWORD, *_PASSPHRASE, *_API_KEY, *_PRIVATE_KEY, *_CLIENT_SECRET; vendor prefix carve-outs keep ANTHROPIC_API_KEY and friends. Users can override via --setenv KEY=VALUE and --unsetenv KEY (and the corresponding TOML keys), or opt out of the built-in policy entirely with --no-env-filter.
This commit is contained in:
+93
-1
@@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::ffi::OsString;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -44,6 +44,12 @@ pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfi
|
||||
globals.seccomp,
|
||||
true,
|
||||
),
|
||||
env_filter: merge_flag_with_default(
|
||||
merge_flag_pair(args.env_filter, args.no_env_filter),
|
||||
profile.env_filter,
|
||||
globals.env_filter,
|
||||
true,
|
||||
),
|
||||
dry_run: merge_flag(
|
||||
merge_flag_pair(args.dry_run, args.no_dry_run),
|
||||
profile.dry_run,
|
||||
@@ -53,6 +59,8 @@ pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfi
|
||||
extra_rw: merge_paths(args.extra_rw, &profile.rw, &globals.rw)?,
|
||||
extra_ro: merge_paths(args.extra_ro, &profile.ro, &globals.ro)?,
|
||||
mask: merge_vecs(args.mask, &profile.mask, &globals.mask),
|
||||
setenv: merge_setenv(args.setenv, &profile.setenv, &globals.setenv),
|
||||
unsetenv: merge_vecs(args.unsetenv, &profile.unsetenv, &globals.unsetenv),
|
||||
bwrap_args: split_bwrap_args(merge_vecs(
|
||||
args.bwrap_args,
|
||||
&profile.bwrap_args,
|
||||
@@ -158,6 +166,21 @@ fn merge_vecs<T: Clone>(cli: Vec<T>, profile: &[T], globals: &[T]) -> Vec<T> {
|
||||
globals.iter().chain(profile).cloned().chain(cli).collect()
|
||||
}
|
||||
|
||||
fn merge_setenv(
|
||||
cli: Vec<(String, String)>,
|
||||
profile: &BTreeMap<String, String>,
|
||||
globals: &BTreeMap<String, String>,
|
||||
) -> Vec<(String, String)> {
|
||||
let mut merged: BTreeMap<String, String> = globals.clone();
|
||||
for (k, v) in profile {
|
||||
merged.insert(k.clone(), v.clone());
|
||||
}
|
||||
for (k, v) in cli {
|
||||
merged.insert(k, v);
|
||||
}
|
||||
merged.into_iter().collect()
|
||||
}
|
||||
|
||||
fn resolve_command(
|
||||
cli_entrypoint: Option<String>,
|
||||
mut passthrough_args: Vec<OsString>,
|
||||
@@ -260,6 +283,7 @@ pub struct Options {
|
||||
pub hardened: Option<bool>,
|
||||
pub unshare_net: Option<bool>,
|
||||
pub seccomp: Option<bool>,
|
||||
pub env_filter: Option<bool>,
|
||||
pub entrypoint: Option<CommandValue>,
|
||||
pub command: Option<CommandValue>,
|
||||
pub dry_run: Option<bool>,
|
||||
@@ -271,6 +295,10 @@ pub struct Options {
|
||||
#[serde(default)]
|
||||
pub mask: Vec<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub setenv: BTreeMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub unsetenv: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub bwrap_args: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -908,6 +936,70 @@ mod tests {
|
||||
assert_eq!(config.chdir, std::fs::canonicalize("/tmp").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_setenv_merges_globals_profile_cli() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
setenv: BTreeMap::from([
|
||||
("A".into(), "global".into()),
|
||||
("B".into(), "global".into()),
|
||||
]),
|
||||
..Options::default()
|
||||
},
|
||||
profile: HashMap::from([(
|
||||
"p".into(),
|
||||
Options {
|
||||
setenv: BTreeMap::from([
|
||||
("B".into(), "profile".into()),
|
||||
("C".into(), "profile".into()),
|
||||
]),
|
||||
..Options::default()
|
||||
},
|
||||
)]),
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("p".into()),
|
||||
setenv: vec![("C".into(), "cli".into()), ("D".into(), "cli".into())],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
assert_eq!(
|
||||
config.setenv,
|
||||
vec![
|
||||
("A".into(), "global".into()),
|
||||
("B".into(), "profile".into()),
|
||||
("C".into(), "cli".into()),
|
||||
("D".into(), "cli".into()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_unsetenv_accumulates() {
|
||||
let file_config = FileConfig {
|
||||
options: Options {
|
||||
unsetenv: vec!["G".into()],
|
||||
..Options::default()
|
||||
},
|
||||
profile: HashMap::from([(
|
||||
"p".into(),
|
||||
Options {
|
||||
unsetenv: vec!["P".into()],
|
||||
..Options::default()
|
||||
},
|
||||
)]),
|
||||
..FileConfig::default()
|
||||
};
|
||||
let args = Args {
|
||||
profile: Some("p".into()),
|
||||
unsetenv: vec!["C".into()],
|
||||
..args_with_command()
|
||||
};
|
||||
let config = build(args, Some(file_config)).unwrap();
|
||||
assert_eq!(config.unsetenv, vec!["G", "P", "C"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_mask_accumulates() {
|
||||
let file_config = FileConfig {
|
||||
|
||||
Reference in New Issue
Block a user