Restrict fail-closed MDM requirements handling

This commit is contained in:
Eva Wong
2026-03-18 11:13:51 -07:00
parent c1840122f5
commit 5b60e97fdc
2 changed files with 101 additions and 7 deletions

View File

@@ -193,12 +193,10 @@ fn parse_managed_config_base64(encoded: &str) -> io::Result<Option<ManagedAdminC
}
fn is_invalid_security_managed_config_entry(dropped_entry: &str) -> bool {
let path = dropped_entry
.split_once(':')
.map_or(dropped_entry, |(path, _)| path)
.trim();
let top_level = path.split(['.', '[']).next().unwrap_or(path);
matches!(top_level, "approval_policy" | "sandbox_mode")
matches!(
managed_entry_top_level_key(dropped_entry),
"approval_policy" | "sandbox_mode"
)
}
fn parse_managed_requirements_base64(encoded: &str) -> io::Result<ConfigRequirementsToml> {
@@ -210,7 +208,44 @@ fn parse_managed_requirements_base64(encoded: &str) -> io::Result<ConfigRequirem
)
})?;
toml::from_str::<ConfigRequirementsToml>(&raw_toml).map_err(|err| {
let parsed = toml::from_str::<TomlValue>(&raw_toml).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Error parsing managed requirements from {source}: {err}"),
)
})?;
let TomlValue::Table(parsed) = parsed else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Error parsing managed requirements from {source}: root must be a table"),
));
};
let sanitized = sanitize_toml_value::<ConfigRequirementsToml>(TomlValue::Table(parsed))
.map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Error parsing managed requirements from {source}: {err}"),
)
})?;
if let Some(dropped_entry) = sanitized
.dropped_entries
.iter()
.find(|entry| is_invalid_security_managed_requirements_entry(entry))
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Error parsing managed requirements from {source}: {dropped_entry}"),
));
}
for dropped_entry in &sanitized.dropped_entries {
tracing::warn!(
dropped_entry = %dropped_entry,
"Ignoring invalid MDM managed requirements entry",
);
}
sanitized.value.try_into().map_err(|err: toml::de::Error| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Error parsing managed requirements from {source}: {err}"),
@@ -218,6 +253,21 @@ fn parse_managed_requirements_base64(encoded: &str) -> io::Result<ConfigRequirem
})
}
fn is_invalid_security_managed_requirements_entry(dropped_entry: &str) -> bool {
matches!(
managed_entry_top_level_key(dropped_entry),
"allowed_approval_policies" | "allowed_sandbox_modes" | "allowed_web_search_modes"
)
}
fn managed_entry_top_level_key(dropped_entry: &str) -> &str {
let path = dropped_entry
.split_once(':')
.map_or(dropped_entry, |(path, _)| path)
.trim();
path.split(['.', '[']).next().unwrap_or(path)
}
fn decode_managed_preferences_base64(encoded: &str) -> io::Result<String> {
String::from_utf8(BASE64_STANDARD.decode(encoded.as_bytes()).map_err(|err| {
tracing::error!("Failed to decode managed value as base64: {err}",);

View File

@@ -578,6 +578,50 @@ async fn managed_preferences_invalid_requirements_fail_closed() -> anyhow::Resul
Ok(())
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn managed_preferences_ignore_invalid_non_security_requirements_entry() -> anyhow::Result<()>
{
use base64::Engine;
let tmp = tempdir()?;
let state = load_config_layers_state(
tmp.path(),
Some(AbsolutePathBuf::try_from(tmp.path())?),
&[] as &[(String, TomlValue)],
LoaderOverrides {
managed_config_path: Some(tmp.path().join("managed_config.toml")),
managed_preferences_base64: Some(String::new()),
macos_managed_config_requirements_base64: Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
allowed_sandbox_modes = ["read-only"]
[features]
personality = "bogus"
"#
.as_bytes(),
),
),
},
CloudRequirementsLoader::default(),
)
.await?;
assert_eq!(
state.requirements_toml().allowed_sandbox_modes,
Some(vec![crate::config_loader::SandboxModeRequirement::ReadOnly])
);
assert_eq!(state.requirements_toml().feature_requirements, None);
assert_eq!(
*state.requirements().sandbox_policy.get(),
SandboxPolicy::new_read_only_policy()
);
Ok(())
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn managed_preferences_requirements_are_applied() -> anyhow::Result<()> {