mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
fix(security): require explicit SANDBOX_ALLOW_FULL_ACCESS to enable FullAccess policy (#967)
* fix(security): require explicit SANDBOX_ALLOW_FULL_ACCESS to enable FullAccess policy FullAccess policy bypasses Docker entirely and runs commands via sh -c directly on the host. Previously, setting SANDBOX_POLICY=full_access alone was sufficient to enable this, which could be triggered accidentally or via prompt injection if tool approval is bypassed. This adds a double opt-in guard: - New SANDBOX_ALLOW_FULL_ACCESS=true env var must ALSO be set for FullAccess to take effect. Without it, the policy is downgraded to WorkspaceWrite with a tracing::error! log. - At execution time, every FullAccess command emits a tracing::warn! with the command and working directory for audit visibility. - The FullAccess variant now documents its blast radius (host shell, unrestricted filesystem/network/environment). - SandboxConfig and SandboxModeConfig gain an allow_full_access field, wired through from_env() and the builder. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(sandbox): address review feedback on FullAccess double opt-in - Add doc comment on builder .policy() warning that FullAccess requires .allow_full_access(true) or execution will return SandboxError::Config - Sanitize audit log: log only binary name instead of full command to prevent secret leakage; add [FullAccess] prefix for grep-ability - Add test_builder_full_access_without_allow_returns_error test covering the builder path without explicit allow_full_access(true) - Fix doc comment mismatch: config.rs and SandboxPolicy::FullAccess docs said "will downgrade to WorkspaceWrite" but runtime returns SandboxError::Config -- aligned docs with actual behavior Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix: merge duplicate mod tests; add allow_full_access to struct initializers After upstream merge, src/config/sandbox.rs had two issues: - Duplicate mod tests block (upstream's original tests at line 271 + our new FullAccess guard tests at line 478) caused E0428 compile error - Upstream test struct literals for SandboxModeConfig were missing the new allow_full_access field (E0063) Fixes: merge the two mod tests into one; add allow_full_access: false to the sandbox_mode_config_custom_values and sandbox_mode_to_sandbox_config test struct initializers. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Gabe Hamilton <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Gabe Hamilton
Claude Sonnet 4.6
parent
f48fe95ac4
commit
8bbb43da52
+80
-1
@@ -8,6 +8,13 @@ pub struct SandboxModeConfig {
|
||||
pub enabled: bool,
|
||||
/// Sandbox policy: "readonly", "workspace_write", or "full_access".
|
||||
pub policy: String,
|
||||
/// Explicit opt-in for `FullAccess` policy.
|
||||
///
|
||||
/// When `policy` is `full_access` but this is `false`, the policy is
|
||||
/// downgraded to `workspace_write` with a loud error log. This prevents
|
||||
/// accidental host-level command execution from a single misconfigured
|
||||
/// env var.
|
||||
pub allow_full_access: bool,
|
||||
/// Command timeout in seconds.
|
||||
pub timeout_secs: u64,
|
||||
/// Memory limit in megabytes.
|
||||
@@ -31,6 +38,7 @@ impl Default for SandboxModeConfig {
|
||||
Self {
|
||||
enabled: true,
|
||||
policy: "readonly".to_string(),
|
||||
allow_full_access: false,
|
||||
timeout_secs: 120,
|
||||
memory_limit_mb: 2048,
|
||||
cpu_shares: 1024,
|
||||
@@ -70,6 +78,7 @@ impl SandboxModeConfig {
|
||||
Ok(Self {
|
||||
enabled: parse_bool_env("SANDBOX_ENABLED", true)?,
|
||||
policy: parse_string_env("SANDBOX_POLICY", "readonly")?,
|
||||
allow_full_access: parse_bool_env("SANDBOX_ALLOW_FULL_ACCESS", false)?,
|
||||
timeout_secs: parse_optional_env("SANDBOX_TIMEOUT_SECS", 120)?,
|
||||
memory_limit_mb: parse_optional_env("SANDBOX_MEMORY_LIMIT_MB", 2048)?,
|
||||
cpu_shares: parse_optional_env("SANDBOX_CPU_SHARES", 1024)?,
|
||||
@@ -82,11 +91,25 @@ impl SandboxModeConfig {
|
||||
}
|
||||
|
||||
/// Convert to SandboxConfig for the sandbox module.
|
||||
///
|
||||
/// If `policy` is `FullAccess` but `allow_full_access` is `false`,
|
||||
/// the policy is downgraded to `WorkspaceWrite` and an error is logged.
|
||||
pub fn to_sandbox_config(&self) -> crate::sandbox::SandboxConfig {
|
||||
use crate::sandbox::SandboxPolicy;
|
||||
use std::time::Duration;
|
||||
|
||||
let policy = self.policy.parse().unwrap_or(SandboxPolicy::ReadOnly);
|
||||
let mut policy = self.policy.parse().unwrap_or(SandboxPolicy::ReadOnly);
|
||||
|
||||
// Double opt-in guard: FullAccess requires SANDBOX_ALLOW_FULL_ACCESS=true
|
||||
if policy == SandboxPolicy::FullAccess && !self.allow_full_access {
|
||||
tracing::error!(
|
||||
"SANDBOX_POLICY=full_access is set but SANDBOX_ALLOW_FULL_ACCESS is not \
|
||||
set to 'true'. FullAccess bypasses Docker and runs commands directly on \
|
||||
the host. Downgrading to WorkspaceWrite for safety. Set \
|
||||
SANDBOX_ALLOW_FULL_ACCESS=true to explicitly enable FullAccess."
|
||||
);
|
||||
policy = SandboxPolicy::WorkspaceWrite;
|
||||
}
|
||||
|
||||
let mut allowlist = crate::sandbox::default_allowlist();
|
||||
allowlist.extend(self.extra_allowed_domains.clone());
|
||||
@@ -94,6 +117,7 @@ impl SandboxModeConfig {
|
||||
crate::sandbox::SandboxConfig {
|
||||
enabled: self.enabled,
|
||||
policy,
|
||||
allow_full_access: self.allow_full_access,
|
||||
timeout: Duration::from_secs(self.timeout_secs),
|
||||
memory_limit_mb: self.memory_limit_mb,
|
||||
cpu_shares: self.cpu_shares,
|
||||
@@ -302,6 +326,7 @@ mod tests {
|
||||
extra_allowed_domains: vec!["example.com".to_string()],
|
||||
reaper_interval_secs: 300,
|
||||
orphan_threshold_secs: 600,
|
||||
allow_full_access: false,
|
||||
};
|
||||
assert!(!cfg.enabled);
|
||||
assert_eq!(cfg.policy, "full_access");
|
||||
@@ -326,6 +351,7 @@ mod tests {
|
||||
extra_allowed_domains: vec!["custom.example.com".to_string()],
|
||||
reaper_interval_secs: 300,
|
||||
orphan_threshold_secs: 600,
|
||||
allow_full_access: false,
|
||||
};
|
||||
let sc = mode.to_sandbox_config();
|
||||
assert!(sc.enabled);
|
||||
@@ -485,4 +511,57 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_access_downgraded_without_allow() {
|
||||
let config = SandboxModeConfig {
|
||||
policy: "full_access".to_string(),
|
||||
allow_full_access: false,
|
||||
..Default::default()
|
||||
};
|
||||
let sandbox = config.to_sandbox_config();
|
||||
// Should have been downgraded to WorkspaceWrite
|
||||
assert_eq!(
|
||||
sandbox.policy,
|
||||
crate::sandbox::SandboxPolicy::WorkspaceWrite
|
||||
);
|
||||
assert!(!sandbox.allow_full_access);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_access_allowed_with_explicit_opt_in() {
|
||||
let config = SandboxModeConfig {
|
||||
policy: "full_access".to_string(),
|
||||
allow_full_access: true,
|
||||
..Default::default()
|
||||
};
|
||||
let sandbox = config.to_sandbox_config();
|
||||
assert_eq!(sandbox.policy, crate::sandbox::SandboxPolicy::FullAccess);
|
||||
assert!(sandbox.allow_full_access);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_full_access_policy_unaffected() {
|
||||
let config = SandboxModeConfig {
|
||||
policy: "workspace_write".to_string(),
|
||||
allow_full_access: false,
|
||||
..Default::default()
|
||||
};
|
||||
let sandbox = config.to_sandbox_config();
|
||||
assert_eq!(
|
||||
sandbox.policy,
|
||||
crate::sandbox::SandboxPolicy::WorkspaceWrite
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readonly_policy_unaffected() {
|
||||
let config = SandboxModeConfig {
|
||||
policy: "readonly".to_string(),
|
||||
allow_full_access: false,
|
||||
..Default::default()
|
||||
};
|
||||
let sandbox = config.to_sandbox_config();
|
||||
assert_eq!(sandbox.policy, crate::sandbox::SandboxPolicy::ReadOnly);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user