Reject unknown config keys

This commit is contained in:
2026-04-01 23:51:47 +02:00
parent c7c4c673cb
commit db60fb9ddb
3 changed files with 69 additions and 4 deletions

View File

@@ -145,6 +145,9 @@ pub struct FileConfig {
pub options: Options, pub options: Options,
#[serde(default)] #[serde(default)]
pub profile: HashMap<String, Options>, pub profile: HashMap<String, Options>,
// Collects unrecognized keys; deny_unknown_fields is incompatible with flatten.
#[serde(flatten)]
_unknown: HashMap<String, toml::Value>,
} }
impl FileConfig { impl FileConfig {
@@ -153,12 +156,26 @@ impl FileConfig {
path: path.to_path_buf(), path: path.to_path_buf(),
source: e, source: e,
})?; })?;
toml::from_str(&contents).map_err(|e| SandboxError::ConfigParse { Self::parse(&contents).map_err(|e| match e {
path: path.to_path_buf(), SandboxError::ConfigParse { source, .. } => SandboxError::ConfigParse {
source: e, path: path.to_path_buf(),
source,
},
other => other,
}) })
} }
fn parse(contents: &str) -> Result<Self, SandboxError> {
let config: Self = toml::from_str(contents).map_err(|e| SandboxError::ConfigParse {
path: PathBuf::new(),
source: e,
})?;
if let Some(key) = config._unknown.keys().next() {
return Err(SandboxError::UnknownConfigKey(key.clone()));
}
Ok(config)
}
fn resolve_profile(&self, name: Option<&str>) -> Result<Options, SandboxError> { fn resolve_profile(&self, name: Option<&str>) -> Result<Options, SandboxError> {
match name { match name {
Some(n) => self Some(n) => self
@@ -172,7 +189,7 @@ impl FileConfig {
} }
#[derive(Deserialize, Default, Clone)] #[derive(Deserialize, Default, Clone)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Options { pub struct Options {
pub blacklist: Option<bool>, pub blacklist: Option<bool>,
pub whitelist: Option<bool>, pub whitelist: Option<bool>,
@@ -396,6 +413,7 @@ mod tests {
..Options::default() ..Options::default()
}, },
)]), )]),
..FileConfig::default()
}; };
let args = Args { let args = Args {
profile: Some("relaxed".into()), profile: Some("relaxed".into()),
@@ -461,6 +479,7 @@ mod tests {
..Options::default() ..Options::default()
}, },
)]), )]),
..FileConfig::default()
}; };
let args = Args { let args = Args {
profile: Some("extra".into()), profile: Some("extra".into()),
@@ -603,6 +622,7 @@ mod tests {
..Options::default() ..Options::default()
}, },
)]), )]),
..FileConfig::default()
}; };
let args = Args { let args = Args {
profile: Some("extra".into()), profile: Some("extra".into()),
@@ -613,6 +633,31 @@ mod tests {
assert_eq!(config.mask.len(), 3); assert_eq!(config.mask.len(), 3);
} }
#[test]
fn unknown_option_rejected() {
let toml = r#"
hardened = true
bogus = "nope"
"#;
assert!(matches!(
FileConfig::parse(toml),
Err(SandboxError::UnknownConfigKey(_))
));
}
#[test]
fn unknown_profile_option_rejected() {
let toml = r#"
[profile.test]
hardened = true
frobnicate = 42
"#;
assert!(matches!(
FileConfig::parse(toml),
Err(SandboxError::ConfigParse { .. })
));
}
fn assert_paths(actual: &[PathBuf], expected: &[&str]) { fn assert_paths(actual: &[PathBuf], expected: &[&str]) {
let expected: Vec<PathBuf> = expected.iter().map(PathBuf::from).collect(); let expected: Vec<PathBuf> = expected.iter().map(PathBuf::from).collect();
assert_eq!(actual, &expected); assert_eq!(actual, &expected);

View File

@@ -22,6 +22,7 @@ pub enum SandboxError {
}, },
ProfileNotFound(String), ProfileNotFound(String),
ConflictingMode, ConflictingMode,
UnknownConfigKey(String),
ConfigPathNotAbsolute(PathBuf), ConfigPathNotAbsolute(PathBuf),
} }
@@ -60,6 +61,7 @@ impl std::fmt::Display for SandboxError {
f, f,
"config section sets both blacklist and whitelist to true" "config section sets both blacklist and whitelist to true"
), ),
Self::UnknownConfigKey(key) => write!(f, "unknown config key: {key}"),
Self::ConfigPathNotAbsolute(p) => { Self::ConfigPathNotAbsolute(p) => {
write!(f, "config path is not absolute: {}", p.display()) write!(f, "config path is not absolute: {}", p.display())
} }

View File

@@ -578,6 +578,24 @@ fn config_invalid_toml_errors() {
); );
} }
#[test]
fn config_unknown_key_errors() {
let dir = TempDir::new().unwrap();
let cfg = write_config(&dir, "hardened = true\nbogus = \"nope\"\n");
let output = sandbox(&["--config", &cfg])
.args(["--", "true"])
.output()
.expect("failed to execute");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("unknown config key"),
"expected unknown key error, got: {stderr}"
);
}
#[test] #[test]
fn mask_hides_directory() { fn mask_hides_directory() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();