fix(sandboxing): use network-only seatbelt for managed yolo

Co-authored-by: Codex noreply@openai.com
This commit is contained in:
viyatb-oai
2026-04-10 13:12:22 -07:00
parent 87328976f6
commit 9f50a2f18f
5 changed files with 346 additions and 6 deletions

View File

@@ -8,10 +8,14 @@ use crate::policy_transforms::should_require_platform_sandbox;
#[cfg(target_os = "macos")]
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
#[cfg(target_os = "macos")]
use crate::seatbelt::create_network_only_seatbelt_command_args;
#[cfg(target_os = "macos")]
use crate::seatbelt::create_seatbelt_command_args_for_policies;
use codex_network_proxy::NetworkProxy;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
#[cfg(target_os = "macos")]
use codex_protocol::permissions::FileSystemSandboxKind;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
@@ -62,6 +66,15 @@ pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option<SandboxType
}
}
#[cfg(target_os = "macos")]
fn should_use_network_only_seatbelt(
file_system_policy: &FileSystemSandboxPolicy,
enforce_managed_network: bool,
) -> bool {
enforce_managed_network
&& matches!(file_system_policy.kind, FileSystemSandboxKind::Unrestricted)
}
#[derive(Debug)]
pub struct SandboxCommand {
pub program: OsString,
@@ -200,14 +213,27 @@ impl SandboxManager {
SandboxType::None => (os_argv_to_strings(argv), None),
#[cfg(target_os = "macos")]
SandboxType::MacosSeatbelt => {
let mut args = create_seatbelt_command_args_for_policies(
os_argv_to_strings(argv),
let command = os_argv_to_strings(argv);
let mut args = if should_use_network_only_seatbelt(
&effective_file_system_policy,
effective_network_policy,
sandbox_policy_cwd,
enforce_managed_network,
network,
);
) {
create_network_only_seatbelt_command_args(
command,
effective_network_policy,
enforce_managed_network,
network,
)
} else {
create_seatbelt_command_args_for_policies(
command,
&effective_file_system_policy,
effective_network_policy,
sandbox_policy_cwd,
enforce_managed_network,
network,
)
};
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
full_command.append(&mut args);

View File

@@ -51,6 +51,57 @@ fn danger_full_access_uses_platform_sandbox_with_network_requirements() {
assert_eq!(sandbox, expected);
}
#[cfg(target_os = "macos")]
#[test]
fn transform_uses_network_only_seatbelt_for_managed_danger_full_access() {
let manager = SandboxManager::new();
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
let exec_request = manager
.transform(SandboxTransformRequest {
command: SandboxCommand {
program: "true".into(),
args: Vec::new(),
cwd: cwd.clone(),
env: HashMap::new(),
additional_permissions: None,
},
policy: &SandboxPolicy::DangerFullAccess,
file_system_policy: &FileSystemSandboxPolicy::unrestricted(),
network_policy: NetworkSandboxPolicy::Enabled,
sandbox: SandboxType::MacosSeatbelt,
enforce_managed_network: true,
network: None,
sandbox_policy_cwd: cwd.as_path(),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
})
.expect("transform");
let policy_index = exec_request
.command
.iter()
.position(|arg| arg == "-p")
.expect("seatbelt command should include policy");
let policy = exec_request
.command
.get(policy_index + 1)
.expect("seatbelt command should include policy text");
assert!(
policy.starts_with("(version 1)\n(allow default)"),
"managed full-access Seatbelt should use the open network-only profile:\n{policy}"
);
assert!(
policy.contains("(deny network-outbound"),
"managed full-access Seatbelt should still enforce network restrictions:\n{policy}"
);
assert!(
!policy.contains("(deny default)"),
"managed full-access Seatbelt should not use the closed base profile:\n{policy}"
);
}
#[test]
fn restricted_file_system_uses_platform_sandbox_without_managed_network() {
let manager = SandboxManager::new();

View File

@@ -15,10 +15,15 @@ use std::path::PathBuf;
use tracing::warn;
use url::Url;
mod network_only;
const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl");
const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl");
const MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS: &str =
include_str!("restricted_read_only_platform_defaults.sbpl");
pub(crate) use network_only::create_network_only_seatbelt_command_args;
#[cfg(test)]
use network_only::network_only_policy_for_network;
/// When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin`
/// to defend against an attacker trying to inject a malicious version on the

View File

@@ -0,0 +1,141 @@
use super::ProxyPolicyInputs;
use super::UnixDomainSocketPolicy;
use super::proxy_policy_inputs;
use super::unix_socket_path_param_key;
use super::unix_socket_path_params;
use codex_network_proxy::NetworkProxy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use std::path::PathBuf;
const MACOS_SEATBELT_NETWORK_ONLY_BASE_POLICY: &str = "(version 1)\n(allow default)";
pub(crate) fn create_network_only_seatbelt_command_args(
command: Vec<String>,
network_sandbox_policy: NetworkSandboxPolicy,
enforce_managed_network: bool,
network: Option<&NetworkProxy>,
) -> Vec<String> {
let proxy = proxy_policy_inputs(network);
let network_policy =
network_only_policy_for_network(network_sandbox_policy, enforce_managed_network, &proxy);
let full_policy = [MACOS_SEATBELT_NETWORK_ONLY_BASE_POLICY, &network_policy].join("\n");
let mut seatbelt_args: Vec<String> = vec!["-p".to_string(), full_policy];
seatbelt_args.extend(
unix_socket_dir_params(&proxy)
.into_iter()
.map(|(key, value): (String, PathBuf)| format!("-D{key}={}", value.to_string_lossy())),
);
seatbelt_args.push("--".to_string());
seatbelt_args.extend(command);
seatbelt_args
}
pub(super) fn network_only_policy_for_network(
network_policy: NetworkSandboxPolicy,
enforce_managed_network: bool,
proxy: &ProxyPolicyInputs,
) -> String {
if network_policy.is_enabled()
&& !enforce_managed_network
&& proxy.ports.is_empty()
&& !proxy.has_proxy_config
{
return String::new();
}
let mut policy = String::new();
push_ip_network_policy(&mut policy, proxy);
push_unix_socket_network_policy(&mut policy, proxy);
policy
}
fn push_ip_network_policy(policy: &mut String, proxy: &ProxyPolicyInputs) {
let mut outbound_exceptions = Vec::new();
if proxy.allow_local_binding {
outbound_exceptions.push(r#"(remote ip "localhost:*")"#.to_string());
}
outbound_exceptions.extend(
proxy
.ports
.iter()
.map(|port| format!(r#"(remote ip "localhost:{port}")"#)),
);
push_deny_rule(
policy,
"network-outbound",
r#"(remote ip "*:*")"#,
&outbound_exceptions,
);
let local_exceptions = if proxy.allow_local_binding {
vec![r#"(local ip "localhost:*")"#.to_string()]
} else {
Vec::new()
};
push_deny_rule(
policy,
"network-bind",
r#"(local ip "*:*")"#,
&local_exceptions,
);
push_deny_rule(
policy,
"network-inbound",
r#"(local ip "*:*")"#,
&local_exceptions,
);
}
fn push_unix_socket_network_policy(policy: &mut String, proxy: &ProxyPolicyInputs) {
let UnixDomainSocketPolicy::Restricted { .. } = proxy.unix_domain_socket_policy else {
return;
};
let exceptions = unix_socket_path_params(proxy)
.into_iter()
.map(|param| {
let key = unix_socket_path_param_key(param.index);
format!(r#"(remote unix-socket (subpath (param "{key}")))"#)
})
.collect::<Vec<_>>();
push_deny_rule(
policy,
"network-outbound",
"(remote unix-socket)",
&exceptions,
);
let exceptions = unix_socket_path_params(proxy)
.into_iter()
.map(|param| {
let key = unix_socket_path_param_key(param.index);
format!(r#"(local unix-socket (subpath (param "{key}")))"#)
})
.collect::<Vec<_>>();
push_deny_rule(policy, "network-bind", "(local unix-socket)", &exceptions);
}
fn push_deny_rule(policy: &mut String, operation: &str, selector: &str, exceptions: &[String]) {
if exceptions.is_empty() {
policy.push_str(&format!("(deny {operation} {selector})\n"));
return;
}
policy.push_str(&format!("(deny {operation} (require-all {selector}"));
for exception in exceptions {
policy.push_str(&format!(" (require-not {exception})"));
}
policy.push_str("))\n");
}
fn unix_socket_dir_params(proxy: &ProxyPolicyInputs) -> Vec<(String, PathBuf)> {
unix_socket_path_params(proxy)
.into_iter()
.map(|param| {
(
unix_socket_path_param_key(param.index),
param.path.into_path_buf(),
)
})
.collect()
}

View File

@@ -2,10 +2,12 @@ use super::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use super::MACOS_SEATBELT_BASE_POLICY;
use super::ProxyPolicyInputs;
use super::UnixDomainSocketPolicy;
use super::create_network_only_seatbelt_command_args;
use super::create_seatbelt_command_args;
use super::create_seatbelt_command_args_for_policies;
use super::dynamic_network_policy;
use super::macos_dir_params;
use super::network_only_policy_for_network;
use super::normalize_path_for_sandbox;
use super::unix_socket_dir_params;
use super::unix_socket_policy;
@@ -307,6 +309,121 @@ fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled() {
);
}
#[test]
fn network_only_policy_denies_direct_ip_except_proxy_ports() {
let policy = network_only_policy_for_network(
NetworkSandboxPolicy::Enabled,
/*enforce_managed_network*/ true,
&ProxyPolicyInputs {
ports: vec![43128],
has_proxy_config: true,
allow_local_binding: false,
..ProxyPolicyInputs::default()
},
);
assert!(
policy.contains(
"(deny network-outbound (require-all (remote ip \"*:*\") (require-not (remote ip \"localhost:43128\"))))"
),
"policy should deny direct outbound IP while preserving proxy access:\n{policy}"
);
assert!(
policy.contains("(deny network-bind (local ip \"*:*\"))"),
"policy should deny local IP binding unless explicitly allowed:\n{policy}"
);
assert!(
policy.contains("(deny network-inbound (local ip \"*:*\"))"),
"policy should deny inbound IP unless explicitly allowed:\n{policy}"
);
assert!(
!policy.contains("(deny default)"),
"network-only policy should not use the closed Seatbelt base policy:\n{policy}"
);
}
#[test]
fn network_only_policy_allows_loopback_when_local_binding_is_enabled() {
let policy = network_only_policy_for_network(
NetworkSandboxPolicy::Enabled,
/*enforce_managed_network*/ true,
&ProxyPolicyInputs {
ports: vec![43128],
has_proxy_config: true,
allow_local_binding: true,
..ProxyPolicyInputs::default()
},
);
assert!(
policy.contains("(require-not (remote ip \"localhost:*\"))"),
"policy should preserve loopback outbound when local binding is allowed:\n{policy}"
);
assert!(
policy.contains(
"(deny network-bind (require-all (local ip \"*:*\") (require-not (local ip \"localhost:*\"))))"
),
"policy should only allow loopback binding when local binding is enabled:\n{policy}"
);
assert!(
policy.contains(
"(deny network-inbound (require-all (local ip \"*:*\") (require-not (local ip \"localhost:*\"))))"
),
"policy should only allow loopback inbound when local binding is enabled:\n{policy}"
);
}
#[test]
fn network_only_policy_respects_unix_socket_allowlist() {
let policy = network_only_policy_for_network(
NetworkSandboxPolicy::Enabled,
/*enforce_managed_network*/ true,
&ProxyPolicyInputs {
unix_domain_socket_policy: UnixDomainSocketPolicy::Restricted {
allowed: vec![absolute_path("/tmp/example.sock")],
},
..ProxyPolicyInputs::default()
},
);
assert!(
policy.contains(
"(deny network-outbound (require-all (remote unix-socket) (require-not (remote unix-socket (subpath (param \"UNIX_SOCKET_PATH_0\"))))))"
),
"policy should deny unix-socket outbound except allowlisted paths:\n{policy}"
);
assert!(
policy.contains(
"(deny network-bind (require-all (local unix-socket) (require-not (local unix-socket (subpath (param \"UNIX_SOCKET_PATH_0\"))))))"
),
"policy should deny unix-socket binding except allowlisted paths:\n{policy}"
);
}
#[test]
fn create_network_only_seatbelt_args_uses_open_base_policy() {
let args = create_network_only_seatbelt_command_args(
vec!["true".to_string()],
NetworkSandboxPolicy::Enabled,
/*enforce_managed_network*/ true,
/*network*/ None,
);
let policy = seatbelt_policy_arg(&args);
assert!(
policy.starts_with("(version 1)\n(allow default)"),
"network-only Seatbelt profile should start from an open default:\n{policy}"
);
assert!(
policy.contains("(deny network-outbound (remote ip \"*:*\")"),
"network-only Seatbelt profile should still deny direct IP outbound:\n{policy}"
);
assert!(
!policy.contains("(deny default)"),
"network-only Seatbelt profile should not inherit the closed base profile:\n{policy}"
);
}
#[test]
fn dynamic_network_policy_preserves_restricted_policy_when_proxy_config_without_ports() {
let policy = dynamic_network_policy(