Root cause
Injection & unsafe execution introduced
feat: expose NetworkMode::ProxyOnly via CapabilitySet.proxy_only()
How AI contributed: Direct introductionPublished Jun 26, 2026 in
nolabs-ai/nono-py
Proxy-only network fallback bypass on older Linux kernels (CWE-693, medium, CVSS 6.4 AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:N): on kernels without Landlock network rules (Landlock ABI < V4 / Linux < 6.7, e.g. 6.1), nono_py.sandboxed_exec() could run CapabilitySet.proxy_only(proxy) without supervising the seccomp-notify proxy-only fallback, so a sandboxed child could strip HTTP_PROXY/HTTPS_PROXY or use raw sockets and open direct TCP connections (e.g. to cloud metadata 169.254.169.254) that proxy-only policy should have denied.
Root cause
feat: expose NetworkMode::ProxyOnly via CapabilitySet.proxy_only()
Fix
The fix treats every policy group with proxy settings as network-blocked by default, so direct sockets are denied and egress must use the supervised proxy path.
Case facts
@@ -296,6 +296,35 @@ impl CapabilitySet { self.inner.set_network_blocked(true); } + /// Restrict network to proxy-only mode.+ ///+ /// Blocks all outbound network at the kernel level (Landlock on Linux,+ /// Seatbelt on macOS) except localhost TCP to the proxy's port. This+ /// ensures the child process can only reach the network through the+ /// proxy, which enforces domain-level filtering.+ ///+ /// Use ``proxy.env_vars()`` to get the environment variables+ /// (HTTP_PROXY, HTTPS_PROXY, etc.) to pass to ``sandboxed_exec(env=...)``.+ ///+ /// Args:+ /// proxy: A running ProxyHandle from ``start_proxy()``+ ///+ /// Example:+ /// >>> proxy = start_proxy(ProxyConfig(allowed_hosts=["example.com"]))+ /// >>> caps = CapabilitySet()+ /// >>> caps.allow_path("/usr", AccessMode.READ)+ /// >>> caps.proxy_only(proxy)+ /// >>> env = list(proxy.env_vars().items())+ /// >>> result = sandboxed_exec(caps, ["curl", "https://example.com"], env=env)+ /// >>> proxy.shutdown()+ fn proxy_only(&mut self, proxy: &proxy::ProxyHandle) {+ use nono::NetworkMode;+ self.inner.set_network_mode_mut(NetworkMode::ProxyOnly {+ port: proxy.port_number(),+ bind_ports: Vec::new(),+ });+ }+ /// Add a raw platform-specific sandbox rule. /// /// On macOS, this is a Seatbelt S-expression string injected verbatim@@ -359,11 +388,7 @@ impl CapabilitySet { fn __repr__(&self) -> String { let n_fs = self.inner.fs_capabilities().len();- let net = if self.inner.is_network_blocked() {- "blocked"- } else {- "allowed"- };+ let net = format!("{}", self.inner.network_mode()); format!("CapabilitySet(fs={}, network={})", n_fs, net) } }@@ -13,11 +13,30 @@ use std::path::{Path, PathBuf}; const EMBEDDED_POLICY_JSON: &str = include_str!("../data/policy.json"); #[derive(Debug, Clone, Deserialize)]+#[serde(deny_unknown_fields)] pub struct RustPolicy {+ #[serde(default)]+ #[allow(dead_code)]+ pub meta: Option<PolicyMeta>, pub groups: HashMap<String, Group>,+ #[serde(default)]+ #[allow(dead_code)]+ pub profiles: Option<serde_json::Value>, } #[derive(Debug, Clone, Deserialize)]+#[serde(deny_unknown_fields)]+pub struct PolicyMeta {+ #[serde(default)]+ #[allow(dead_code)]+ pub version: Option<u32>,+ #[serde(default)]+ #[allow(dead_code)]+ pub schema_version: Option<String>,+}++#[derive(Debug, Clone, Deserialize)]+#[serde(deny_unknown_fields)] pub struct Group { #[allow(dead_code)] pub description: String,@@ -36,6 +55,7 @@ pub struct Group { } #[derive(Debug, Clone, Default, Deserialize)]+#[serde(deny_unknown_fields)] pub struct AllowOps { #[serde(default)] pub read: Vec<String>,@@ -46,6 +66,7 @@ pub struct AllowOps { } #[derive(Debug, Clone, Default, Deserialize)]+#[serde(deny_unknown_fields)] pub struct DenyOps { #[serde(default)] pub access: Vec<String>,@@ -56,6 +77,7 @@ pub struct DenyOps { } #[derive(Debug, Clone, Default, Deserialize)]+#[serde(deny_unknown_fields)] pub struct NetworkOps { #[serde(default)] pub block: bool,@@ -79,6 +101,7 @@ pub struct NetworkOps { } #[derive(Debug, Clone, Deserialize)]+#[serde(deny_unknown_fields)] pub struct PolicyRouteConfig { pub prefix: String, pub upstream: String,@@ -109,6 +132,7 @@ pub struct PolicyRouteConfig { } #[derive(Debug, Clone, Deserialize)]+#[serde(deny_unknown_fields)] pub struct PolicyEndpointRule { pub method: String, pub path: String,@@ -501,10 +525,10 @@ fn resolve_single_group( } } - if let Some(network) = &group.network- && network.block- {- caps.inner.set_network_blocked(true);+ if let Some(network) = &group.network {+ if network.block || network_requires_proxy(network) {+ caps.inner.set_network_blocked(true);+ } } if cfg!(target_os = "macos")@@ -521,6 +545,15 @@ fn resolve_single_group( Ok(needs_unlink_overrides) } AI-assisted change 55d71a17540d6eb47c1337ad063f70c7a4cbbaf22df73c730f985a139f0ca8ec · Fix 2e750dca2ba941b8bf3d0adc86fe7872a06e020a3e990c76c947cdd9401d6000