Windows Sandbox: Show Everyone-writable directory warning (#6283)

Show a warning when Auto Sandbox mode becomes enabled, if we detect
Everyone-writable directories, since they cannot be protected by the
current implementation of the Sandbox.

This PR also includes changes to how we detect Everyone-writable to be
*much* faster
This commit is contained in:
iceweasel-oai
2025-11-06 10:44:42 -08:00
committed by GitHub
parent dbad5eeec6
commit 871d442b8e
10 changed files with 497 additions and 54 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -1452,6 +1452,7 @@ dependencies = [
"codex-login", "codex-login",
"codex-ollama", "codex-ollama",
"codex-protocol", "codex-protocol",
"codex-windows-sandbox",
"color-eyre", "color-eyre",
"crossterm", "crossterm",
"diffy", "diffy",

View File

@@ -87,7 +87,7 @@ codex-utils-pty = { path = "utils/pty" }
codex-utils-readiness = { path = "utils/readiness" } codex-utils-readiness = { path = "utils/readiness" }
codex-utils-string = { path = "utils/string" } codex-utils-string = { path = "utils/string" }
codex-utils-tokenizer = { path = "utils/tokenizer" } codex-utils-tokenizer = { path = "utils/tokenizer" }
codex-windows-sandbox = { path = "windows-sandbox" } codex-windows-sandbox = { path = "windows-sandbox-rs" }
core_test_support = { path = "core/tests/common" } core_test_support = { path = "core/tests/common" }
mcp-types = { path = "mcp-types" } mcp-types = { path = "mcp-types" }
mcp_test_support = { path = "mcp-server/tests/common" } mcp_test_support = { path = "mcp-server/tests/common" }

View File

@@ -23,6 +23,8 @@ pub enum ConfigEdit {
}, },
/// Toggle the acknowledgement flag under `[notice]`. /// Toggle the acknowledgement flag under `[notice]`.
SetNoticeHideFullAccessWarning(bool), SetNoticeHideFullAccessWarning(bool),
/// Toggle the Windows world-writable directories warning acknowledgement flag.
SetNoticeHideWorldWritableWarning(bool),
/// Toggle the Windows onboarding acknowledgement flag. /// Toggle the Windows onboarding acknowledgement flag.
SetWindowsWslSetupAcknowledged(bool), SetWindowsWslSetupAcknowledged(bool),
/// Replace the entire `[mcp_servers]` table. /// Replace the entire `[mcp_servers]` table.
@@ -239,6 +241,11 @@ impl ConfigDocument {
&[Notice::TABLE_KEY, "hide_full_access_warning"], &[Notice::TABLE_KEY, "hide_full_access_warning"],
value(*acknowledged), value(*acknowledged),
)), )),
ConfigEdit::SetNoticeHideWorldWritableWarning(acknowledged) => Ok(self.write_value(
Scope::Global,
&[Notice::TABLE_KEY, "hide_world_writable_warning"],
value(*acknowledged),
)),
ConfigEdit::SetWindowsWslSetupAcknowledged(acknowledged) => Ok(self.write_value( ConfigEdit::SetWindowsWslSetupAcknowledged(acknowledged) => Ok(self.write_value(
Scope::Global, Scope::Global,
&["windows_wsl_setup_acknowledged"], &["windows_wsl_setup_acknowledged"],
@@ -473,6 +480,12 @@ impl ConfigEditsBuilder {
self self
} }
pub fn set_hide_world_writable_warning(mut self, acknowledged: bool) -> Self {
self.edits
.push(ConfigEdit::SetNoticeHideWorldWritableWarning(acknowledged));
self
}
pub fn set_windows_wsl_setup_acknowledged(mut self, acknowledged: bool) -> Self { pub fn set_windows_wsl_setup_acknowledged(mut self, acknowledged: bool) -> Self {
self.edits self.edits
.push(ConfigEdit::SetWindowsWslSetupAcknowledged(acknowledged)); .push(ConfigEdit::SetWindowsWslSetupAcknowledged(acknowledged));

View File

@@ -358,6 +358,8 @@ pub struct Tui {
pub struct Notice { pub struct Notice {
/// Tracks whether the user has acknowledged the full access warning prompt. /// Tracks whether the user has acknowledged the full access warning prompt.
pub hide_full_access_warning: Option<bool>, pub hide_full_access_warning: Option<bool>,
/// Tracks whether the user has acknowledged the Windows world-writable directories warning.
pub hide_world_writable_warning: Option<bool>,
} }
impl Notice { impl Notice {

View File

@@ -27,6 +27,7 @@ base64 = { workspace = true }
chrono = { workspace = true, features = ["serde"] } chrono = { workspace = true, features = ["serde"] }
clap = { workspace = true, features = ["derive"] } clap = { workspace = true, features = ["derive"] }
codex-ansi-escape = { workspace = true } codex-ansi-escape = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-arg0 = { workspace = true } codex-arg0 = { workspace = true }
codex-common = { workspace = true, features = [ codex-common = { workspace = true, features = [
"cli", "cli",
@@ -34,17 +35,13 @@ codex-common = { workspace = true, features = [
"sandbox_summary", "sandbox_summary",
] } ] }
codex-core = { workspace = true } codex-core = { workspace = true }
codex-feedback = { workspace = true }
codex-file-search = { workspace = true } codex-file-search = { workspace = true }
codex-login = { workspace = true } codex-login = { workspace = true }
codex-ollama = { workspace = true } codex-ollama = { workspace = true }
codex-protocol = { workspace = true } codex-protocol = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-feedback = { workspace = true }
color-eyre = { workspace = true } color-eyre = { workspace = true }
crossterm = { workspace = true, features = [ crossterm = { workspace = true, features = ["bracketed-paste", "event-stream"] }
"bracketed-paste",
"event-stream",
] }
diffy = { workspace = true } diffy = { workspace = true }
dirs = { workspace = true } dirs = { workspace = true }
dunce = { workspace = true } dunce = { workspace = true }
@@ -52,6 +49,7 @@ image = { workspace = true, features = ["jpeg", "png"] }
itertools = { workspace = true } itertools = { workspace = true }
lazy_static = { workspace = true } lazy_static = { workspace = true }
mcp-types = { workspace = true } mcp-types = { workspace = true }
opentelemetry-appender-tracing = { workspace = true }
pathdiff = { workspace = true } pathdiff = { workspace = true }
pulldown-cmark = { workspace = true } pulldown-cmark = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
@@ -71,8 +69,6 @@ strum_macros = { workspace = true }
supports-color = { workspace = true } supports-color = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }
textwrap = { workspace = true } textwrap = { workspace = true }
tree-sitter-highlight = { workspace = true }
tree-sitter-bash = { workspace = true }
tokio = { workspace = true, features = [ tokio = { workspace = true, features = [
"io-std", "io-std",
"macros", "macros",
@@ -85,11 +81,14 @@ toml = { workspace = true }
tracing = { workspace = true, features = ["log"] } tracing = { workspace = true, features = ["log"] }
tracing-appender = { workspace = true } tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] } tracing-subscriber = { workspace = true, features = ["env-filter"] }
opentelemetry-appender-tracing = { workspace = true } tree-sitter-bash = { workspace = true }
tree-sitter-highlight = { workspace = true }
unicode-segmentation = { workspace = true } unicode-segmentation = { workspace = true }
unicode-width = { workspace = true } unicode-width = { workspace = true }
url = { workspace = true } url = { workspace = true }
codex-windows-sandbox = { workspace = true }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = { workspace = true } libc = { workspace = true }
@@ -105,5 +104,5 @@ chrono = { workspace = true, features = ["serde"] }
insta = { workspace = true } insta = { workspace = true }
pretty_assertions = { workspace = true } pretty_assertions = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
vt100 = { workspace = true }
serial_test = { workspace = true } serial_test = { workspace = true }
vt100 = { workspace = true }

View File

@@ -79,6 +79,9 @@ pub(crate) struct App {
pub(crate) feedback: codex_feedback::CodexFeedback, pub(crate) feedback: codex_feedback::CodexFeedback,
/// Set when the user confirms an update; propagated on exit. /// Set when the user confirms an update; propagated on exit.
pub(crate) pending_update_action: Option<UpdateAction>, pub(crate) pending_update_action: Option<UpdateAction>,
// One-shot suppression of the next world-writable scan after user confirmation.
skip_world_writable_scan_once: bool,
} }
impl App { impl App {
@@ -168,8 +171,30 @@ impl App {
backtrack: BacktrackState::default(), backtrack: BacktrackState::default(),
feedback: feedback.clone(), feedback: feedback.clone(),
pending_update_action: None, pending_update_action: None,
skip_world_writable_scan_once: false,
}; };
// On startup, if Auto mode (workspace-write) is active, warn about world-writable dirs on Windows.
#[cfg(target_os = "windows")]
{
let should_check = codex_core::get_platform_sandbox().is_some()
&& matches!(
app.config.sandbox_policy,
codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. }
)
&& !app
.config
.notices
.hide_world_writable_warning
.unwrap_or(false);
if should_check {
let cwd = app.config.cwd.clone();
let env_map: std::collections::HashMap<String, String> = std::env::vars().collect();
let tx = app.app_event_tx.clone();
Self::spawn_world_writable_scan(cwd, env_map, tx, false);
}
}
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
if let Some(latest_version) = upgrade_version { if let Some(latest_version) = upgrade_version {
app.handle_event( app.handle_event(
@@ -360,6 +385,10 @@ impl App {
AppEvent::OpenFullAccessConfirmation { preset } => { AppEvent::OpenFullAccessConfirmation { preset } => {
self.chat_widget.open_full_access_confirmation(preset); self.chat_widget.open_full_access_confirmation(preset);
} }
AppEvent::OpenWorldWritableWarningConfirmation { preset } => {
self.chat_widget
.open_world_writable_warning_confirmation(preset);
}
AppEvent::OpenFeedbackNote { AppEvent::OpenFeedbackNote {
category, category,
include_logs, include_logs,
@@ -418,11 +447,45 @@ impl App {
self.chat_widget.set_approval_policy(policy); self.chat_widget.set_approval_policy(policy);
} }
AppEvent::UpdateSandboxPolicy(policy) => { AppEvent::UpdateSandboxPolicy(policy) => {
#[cfg(target_os = "windows")]
let policy_is_workspace_write = matches!(
policy,
codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. }
);
self.chat_widget.set_sandbox_policy(policy); self.chat_widget.set_sandbox_policy(policy);
// If sandbox policy becomes workspace-write, run the Windows world-writable scan.
#[cfg(target_os = "windows")]
{
// One-shot suppression if the user just confirmed continue.
if self.skip_world_writable_scan_once {
self.skip_world_writable_scan_once = false;
return Ok(true);
}
let should_check = codex_core::get_platform_sandbox().is_some()
&& policy_is_workspace_write
&& !self.chat_widget.world_writable_warning_hidden();
if should_check {
let cwd = self.config.cwd.clone();
let env_map: std::collections::HashMap<String, String> =
std::env::vars().collect();
let tx = self.app_event_tx.clone();
Self::spawn_world_writable_scan(cwd, env_map, tx, false);
}
}
}
AppEvent::SkipNextWorldWritableScan => {
self.skip_world_writable_scan_once = true;
} }
AppEvent::UpdateFullAccessWarningAcknowledged(ack) => { AppEvent::UpdateFullAccessWarningAcknowledged(ack) => {
self.chat_widget.set_full_access_warning_acknowledged(ack); self.chat_widget.set_full_access_warning_acknowledged(ack);
} }
AppEvent::UpdateWorldWritableWarningAcknowledged(ack) => {
self.chat_widget
.set_world_writable_warning_acknowledged(ack);
}
AppEvent::PersistFullAccessWarningAcknowledged => { AppEvent::PersistFullAccessWarningAcknowledged => {
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home) if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.set_hide_full_access_warning(true) .set_hide_full_access_warning(true)
@@ -438,6 +501,21 @@ impl App {
)); ));
} }
} }
AppEvent::PersistWorldWritableWarningAcknowledged => {
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.set_hide_world_writable_warning(true)
.apply()
.await
{
tracing::error!(
error = %err,
"failed to persist world-writable warning acknowledgement"
);
self.chat_widget.add_error_message(format!(
"Failed to save Auto mode warning preference: {err}"
));
}
}
AppEvent::OpenApprovalsPopup => { AppEvent::OpenApprovalsPopup => {
self.chat_widget.open_approvals_popup(); self.chat_widget.open_approvals_popup();
} }
@@ -541,6 +619,31 @@ impl App {
} }
}; };
} }
#[cfg(target_os = "windows")]
fn spawn_world_writable_scan(
cwd: PathBuf,
env_map: std::collections::HashMap<String, String>,
tx: AppEventSender,
apply_preset_on_continue: bool,
) {
tokio::task::spawn_blocking(move || {
if codex_windows_sandbox::preflight_audit_everyone_writable(&cwd, &env_map).is_err() {
if apply_preset_on_continue {
if let Some(preset) = codex_common::approval_presets::builtin_approval_presets()
.into_iter()
.find(|p| p.id == "auto")
{
tx.send(AppEvent::OpenWorldWritableWarningConfirmation {
preset: Some(preset),
});
}
} else {
tx.send(AppEvent::OpenWorldWritableWarningConfirmation { preset: None });
}
}
});
}
} }
#[cfg(test)] #[cfg(test)]
@@ -592,6 +695,7 @@ mod tests {
backtrack: BacktrackState::default(), backtrack: BacktrackState::default(),
feedback: codex_feedback::CodexFeedback::new(), feedback: codex_feedback::CodexFeedback::new(),
pending_update_action: None, pending_update_action: None,
skip_world_writable_scan_once: false,
} }
} }

View File

@@ -72,7 +72,17 @@ pub(crate) enum AppEvent {
preset: ApprovalPreset, preset: ApprovalPreset,
}, },
/// Open the Windows world-writable directories warning.
/// If `preset` is `Some`, the confirmation will apply the provided
/// approval/sandbox configuration on Continue; if `None`, it performs no
/// policy change and only acknowledges/dismisses the warning.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
OpenWorldWritableWarningConfirmation {
preset: Option<ApprovalPreset>,
},
/// Show Windows Subsystem for Linux setup instructions for auto mode. /// Show Windows Subsystem for Linux setup instructions for auto mode.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
ShowWindowsAutoModeInstructions, ShowWindowsAutoModeInstructions,
/// Update the current approval policy in the running app and widget. /// Update the current approval policy in the running app and widget.
@@ -84,9 +94,21 @@ pub(crate) enum AppEvent {
/// Update whether the full access warning prompt has been acknowledged. /// Update whether the full access warning prompt has been acknowledged.
UpdateFullAccessWarningAcknowledged(bool), UpdateFullAccessWarningAcknowledged(bool),
/// Update whether the world-writable directories warning has been acknowledged.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
UpdateWorldWritableWarningAcknowledged(bool),
/// Persist the acknowledgement flag for the full access warning prompt. /// Persist the acknowledgement flag for the full access warning prompt.
PersistFullAccessWarningAcknowledged, PersistFullAccessWarningAcknowledged,
/// Persist the acknowledgement flag for the world-writable directories warning.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
PersistWorldWritableWarningAcknowledged,
/// Skip the next world-writable scan (one-shot) after a user-confirmed continue.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
SkipNextWorldWritableScan,
/// Re-open the approval presets popup. /// Re-open the approval presets popup.
OpenApprovalsPopup, OpenApprovalsPopup,

View File

@@ -2030,13 +2030,34 @@ impl ChatWidget {
preset: preset_clone.clone(), preset: preset_clone.clone(),
}); });
})] })]
} else if cfg!(target_os = "windows") } else if preset.id == "auto" {
&& preset.id == "auto" #[cfg(target_os = "windows")]
&& codex_core::get_platform_sandbox().is_none() {
{ if codex_core::get_platform_sandbox().is_none() {
vec![Box::new(|tx| { vec![Box::new(|tx| {
tx.send(AppEvent::ShowWindowsAutoModeInstructions); tx.send(AppEvent::ShowWindowsAutoModeInstructions);
})] })]
} else if !self
.config
.notices
.hide_world_writable_warning
.unwrap_or(false)
&& self.windows_world_writable_flagged()
{
let preset_clone = preset.clone();
vec![Box::new(move |tx| {
tx.send(AppEvent::OpenWorldWritableWarningConfirmation {
preset: Some(preset_clone.clone()),
});
})]
} else {
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
}
}
#[cfg(not(target_os = "windows"))]
{
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
}
} else { } else {
Self::approval_preset_actions(preset.approval, preset.sandbox.clone()) Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
}; };
@@ -2078,6 +2099,19 @@ impl ChatWidget {
})] })]
} }
#[cfg(target_os = "windows")]
fn windows_world_writable_flagged(&self) -> bool {
use std::collections::HashMap;
let mut env_map: HashMap<String, String> = HashMap::new();
for (k, v) in std::env::vars() {
env_map.insert(k, v);
}
match codex_windows_sandbox::preflight_audit_everyone_writable(&self.config.cwd, &env_map) {
Ok(()) => false,
Err(_) => true,
}
}
pub(crate) fn open_full_access_confirmation(&mut self, preset: ApprovalPreset) { pub(crate) fn open_full_access_confirmation(&mut self, preset: ApprovalPreset) {
let approval = preset.approval; let approval = preset.approval;
let sandbox = preset.sandbox; let sandbox = preset.sandbox;
@@ -2142,6 +2176,95 @@ impl ChatWidget {
}); });
} }
#[cfg(target_os = "windows")]
pub(crate) fn open_world_writable_warning_confirmation(
&mut self,
preset: Option<ApprovalPreset>,
) {
let (approval, sandbox) = match &preset {
Some(p) => (Some(p.approval), Some(p.sandbox.clone())),
None => (None, None),
};
let mut header_children: Vec<Box<dyn Renderable>> = Vec::new();
let title_line = Line::from("Auto mode has unprotected directories").bold();
let info_line = Line::from(vec![
"Some important directories on this system are world-writable. ".into(),
"The Windows sandbox cannot protect writes to these locations in Auto mode."
.fg(Color::Red),
]);
header_children.push(Box::new(title_line));
header_children.push(Box::new(
Paragraph::new(vec![info_line]).wrap(Wrap { trim: false }),
));
let header = ColumnRenderable::with(header_children);
// Build actions ensuring acknowledgement happens before applying the new sandbox policy,
// so downstream policy-change hooks don't re-trigger the warning.
let mut accept_actions: Vec<SelectionAction> = Vec::new();
// Suppress the immediate re-scan once after user confirms continue.
accept_actions.push(Box::new(|tx| {
tx.send(AppEvent::SkipNextWorldWritableScan);
}));
if let (Some(approval), Some(sandbox)) = (approval, sandbox.clone()) {
accept_actions.extend(Self::approval_preset_actions(approval, sandbox));
}
let mut accept_and_remember_actions: Vec<SelectionAction> = Vec::new();
accept_and_remember_actions.push(Box::new(|tx| {
tx.send(AppEvent::UpdateWorldWritableWarningAcknowledged(true));
tx.send(AppEvent::PersistWorldWritableWarningAcknowledged);
}));
if let (Some(approval), Some(sandbox)) = (approval, sandbox) {
accept_and_remember_actions.extend(Self::approval_preset_actions(approval, sandbox));
}
let deny_actions: Vec<SelectionAction> = if preset.is_some() {
vec![Box::new(|tx| {
tx.send(AppEvent::OpenApprovalsPopup);
})]
} else {
Vec::new()
};
let items = vec![
SelectionItem {
name: "Continue".to_string(),
description: Some("Apply Auto mode for this session".to_string()),
actions: accept_actions,
dismiss_on_select: true,
..Default::default()
},
SelectionItem {
name: "Continue and don't warn again".to_string(),
description: Some("Enable Auto mode and remember this choice".to_string()),
actions: accept_and_remember_actions,
dismiss_on_select: true,
..Default::default()
},
SelectionItem {
name: "Cancel".to_string(),
description: Some("Go back without enabling Auto mode".to_string()),
actions: deny_actions,
dismiss_on_select: true,
..Default::default()
},
];
self.bottom_pane.show_selection_view(SelectionViewParams {
footer_hint: Some(standard_popup_hint_line()),
items,
header: Box::new(header),
..Default::default()
});
}
#[cfg(not(target_os = "windows"))]
pub(crate) fn open_world_writable_warning_confirmation(
&mut self,
_preset: Option<ApprovalPreset>,
) {
}
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub(crate) fn open_windows_auto_mode_instructions(&mut self) { pub(crate) fn open_windows_auto_mode_instructions(&mut self) {
use ratatui_macros::line; use ratatui_macros::line;
@@ -2193,6 +2316,18 @@ impl ChatWidget {
self.config.notices.hide_full_access_warning = Some(acknowledged); self.config.notices.hide_full_access_warning = Some(acknowledged);
} }
pub(crate) fn set_world_writable_warning_acknowledged(&mut self, acknowledged: bool) {
self.config.notices.hide_world_writable_warning = Some(acknowledged);
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn world_writable_warning_hidden(&self) -> bool {
self.config
.notices
.hide_world_writable_warning
.unwrap_or(false)
}
/// Set the reasoning effort in the widget's config copy. /// Set the reasoning effort in the widget's config copy.
pub(crate) fn set_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) { pub(crate) fn set_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
self.config.model_reasoning_effort = effort; self.config.model_reasoning_effort = effort;

View File

@@ -1,4 +1,3 @@
use crate::acl::dacl_effective_allows_write;
use crate::token::world_sid; use crate::token::world_sid;
use crate::winutil::to_wide; use crate::winutil::to_wide;
use anyhow::anyhow; use anyhow::anyhow;
@@ -13,8 +12,31 @@ use windows_sys::Win32::Foundation::LocalFree;
use windows_sys::Win32::Foundation::ERROR_SUCCESS; use windows_sys::Win32::Foundation::ERROR_SUCCESS;
use windows_sys::Win32::Foundation::HLOCAL; use windows_sys::Win32::Foundation::HLOCAL;
use windows_sys::Win32::Security::Authorization::GetNamedSecurityInfoW; use windows_sys::Win32::Security::Authorization::GetNamedSecurityInfoW;
use windows_sys::Win32::Security::Authorization::GetSecurityInfo;
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Storage::FileSystem::CreateFileW;
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_DELETE;
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ;
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE;
use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING;
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE;
use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_DATA;
use windows_sys::Win32::Storage::FileSystem::FILE_APPEND_DATA;
use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_EA;
use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_ATTRIBUTES;
const GENERIC_ALL_MASK: u32 = 0x1000_0000;
const GENERIC_WRITE_MASK: u32 = 0x4000_0000;
use windows_sys::Win32::Security::ACL; use windows_sys::Win32::Security::ACL;
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION; use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
use windows_sys::Win32::Security::ACL_SIZE_INFORMATION;
use windows_sys::Win32::Security::AclSizeInformation;
use windows_sys::Win32::Security::GetAclInformation;
use windows_sys::Win32::Security::GetAce;
use windows_sys::Win32::Security::ACCESS_ALLOWED_ACE;
use windows_sys::Win32::Security::ACE_HEADER;
use windows_sys::Win32::Security::EqualSid;
fn unique_push(set: &mut HashSet<PathBuf>, out: &mut Vec<PathBuf>, p: PathBuf) { fn unique_push(set: &mut HashSet<PathBuf>, out: &mut Vec<PathBuf>, p: PathBuf) {
if let Ok(abs) = p.canonicalize() { if let Ok(abs) = p.canonicalize() {
@@ -27,30 +49,22 @@ fn unique_push(set: &mut HashSet<PathBuf>, out: &mut Vec<PathBuf>, p: PathBuf) {
fn gather_candidates(cwd: &Path, env: &std::collections::HashMap<String, String>) -> Vec<PathBuf> { fn gather_candidates(cwd: &Path, env: &std::collections::HashMap<String, String>) -> Vec<PathBuf> {
let mut set: HashSet<PathBuf> = HashSet::new(); let mut set: HashSet<PathBuf> = HashSet::new();
let mut out: Vec<PathBuf> = Vec::new(); let mut out: Vec<PathBuf> = Vec::new();
// Core roots // 1) CWD first (so immediate children get scanned early)
for p in [ unique_push(&mut set, &mut out, cwd.to_path_buf());
PathBuf::from("C:/"), // 2) TEMP/TMP next (often small, quick to scan)
PathBuf::from("C:/Windows"), for k in ["TEMP", "TMP"] {
PathBuf::from("C:/ProgramData"), if let Some(v) = env.get(k).cloned().or_else(|| std::env::var(k).ok()) {
] { unique_push(&mut set, &mut out, PathBuf::from(v));
unique_push(&mut set, &mut out, p); }
} }
// User roots // 3) User roots
if let Some(up) = std::env::var_os("USERPROFILE") { if let Some(up) = std::env::var_os("USERPROFILE") {
unique_push(&mut set, &mut out, PathBuf::from(up)); unique_push(&mut set, &mut out, PathBuf::from(up));
} }
if let Some(pubp) = std::env::var_os("PUBLIC") { if let Some(pubp) = std::env::var_os("PUBLIC") {
unique_push(&mut set, &mut out, PathBuf::from(pubp)); unique_push(&mut set, &mut out, PathBuf::from(pubp));
} }
// CWD // 4) PATH entries (best-effort)
unique_push(&mut set, &mut out, cwd.to_path_buf());
// TEMP/TMP
for k in ["TEMP", "TMP"] {
if let Some(v) = env.get(k).cloned().or_else(|| std::env::var(k).ok()) {
unique_push(&mut set, &mut out, PathBuf::from(v));
}
}
// PATH entries
if let Some(path) = env if let Some(path) = env
.get("PATH") .get("PATH")
.cloned() .cloned()
@@ -62,31 +76,85 @@ fn gather_candidates(cwd: &Path, env: &std::collections::HashMap<String, String>
} }
} }
} }
// 5) Core system roots last
for p in [
PathBuf::from("C:/"),
PathBuf::from("C:/Windows"),
PathBuf::from("C:/ProgramData"),
] {
unique_push(&mut set, &mut out, p);
}
out out
} }
unsafe fn path_has_world_write_allow(path: &Path) -> Result<bool> { unsafe fn path_has_world_write_allow(path: &Path) -> Result<bool> {
// Prefer handle-based query (often faster than name-based), fallback to name-based on error
let mut p_sd: *mut c_void = std::ptr::null_mut(); let mut p_sd: *mut c_void = std::ptr::null_mut();
let mut p_dacl: *mut ACL = std::ptr::null_mut(); let mut p_dacl: *mut ACL = std::ptr::null_mut();
let code = GetNamedSecurityInfoW(
to_wide(path).as_ptr(), let mut try_named = false;
1, let wpath = to_wide(path);
DACL_SECURITY_INFORMATION, let h = CreateFileW(
wpath.as_ptr(),
0x00020000, // READ_CONTROL
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
std::ptr::null_mut(), std::ptr::null_mut(),
std::ptr::null_mut(), OPEN_EXISTING,
&mut p_dacl, FILE_FLAG_BACKUP_SEMANTICS,
std::ptr::null_mut(), 0,
&mut p_sd,
); );
if code != ERROR_SUCCESS { if h == INVALID_HANDLE_VALUE {
if !p_sd.is_null() { try_named = true;
LocalFree(p_sd as HLOCAL); } else {
let code = GetSecurityInfo(
h,
1, // SE_FILE_OBJECT
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut p_dacl,
std::ptr::null_mut(),
&mut p_sd,
);
CloseHandle(h);
if code != ERROR_SUCCESS {
try_named = true;
if !p_sd.is_null() {
LocalFree(p_sd as HLOCAL);
p_sd = std::ptr::null_mut();
p_dacl = std::ptr::null_mut();
}
} }
return Ok(false);
} }
if try_named {
let code = GetNamedSecurityInfoW(
wpath.as_ptr(),
1,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut p_dacl,
std::ptr::null_mut(),
&mut p_sd,
);
if code != ERROR_SUCCESS {
if !p_sd.is_null() {
LocalFree(p_sd as HLOCAL);
}
return Ok(false);
}
}
let mut world = world_sid()?; let mut world = world_sid()?;
let psid_world = world.as_mut_ptr() as *mut c_void; let psid_world = world.as_mut_ptr() as *mut c_void;
let has = dacl_effective_allows_write(p_dacl, psid_world); // Very fast mask-based check for world-writable grants (includes GENERIC_*).
if !dacl_quick_world_write_mask_allows(p_dacl, psid_world) {
if !p_sd.is_null() { LocalFree(p_sd as HLOCAL); }
return Ok(false);
}
// Quick detector flagged a write grant for Everyone: treat as writable.
let has = true;
if !p_sd.is_null() { if !p_sd.is_null() {
LocalFree(p_sd as HLOCAL); LocalFree(p_sd as HLOCAL);
} }
@@ -100,18 +168,41 @@ pub fn audit_everyone_writable(
let start = Instant::now(); let start = Instant::now();
let mut flagged: Vec<PathBuf> = Vec::new(); let mut flagged: Vec<PathBuf> = Vec::new();
let mut checked = 0usize; let mut checked = 0usize;
// Fast path: check CWD immediate children first so workspace issues are caught early.
if let Ok(read) = std::fs::read_dir(cwd) {
for ent in read.flatten().take(250) {
if start.elapsed() > Duration::from_secs(5) || checked > 5000 {
break;
}
let ft = match ent.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
if ft.is_symlink() || !ft.is_dir() {
continue;
}
let p = ent.path();
checked += 1;
let has = unsafe { path_has_world_write_allow(&p)? };
if has {
flagged.push(p);
}
}
}
// Continue with broader candidate sweep
let candidates = gather_candidates(cwd, env); let candidates = gather_candidates(cwd, env);
for root in candidates { for root in candidates {
if start.elapsed() > Duration::from_secs(5) || checked > 5000 { if start.elapsed() > Duration::from_secs(5) || checked > 5000 {
break; break;
} }
checked += 1; checked += 1;
if unsafe { path_has_world_write_allow(&root)? } { let has_root = unsafe { path_has_world_write_allow(&root)? };
if has_root {
flagged.push(root.clone()); flagged.push(root.clone());
} }
// one level down best-effort // one level down best-effort
if let Ok(read) = std::fs::read_dir(&root) { if let Ok(read) = std::fs::read_dir(&root) {
for ent in read.flatten().take(50) { for ent in read.flatten().take(250) {
let p = ent.path(); let p = ent.path();
if start.elapsed() > Duration::from_secs(5) || checked > 5000 { if start.elapsed() > Duration::from_secs(5) || checked > 5000 {
break; break;
@@ -126,22 +217,93 @@ pub fn audit_everyone_writable(
} }
if ft.is_dir() { if ft.is_dir() {
checked += 1; checked += 1;
if unsafe { path_has_world_write_allow(&p)? } { let has_child = unsafe { path_has_world_write_allow(&p)? };
if has_child {
flagged.push(p); flagged.push(p);
} }
} }
} }
} }
} }
let elapsed_ms = start.elapsed().as_millis();
if !flagged.is_empty() { if !flagged.is_empty() {
let mut list = String::new(); let mut list = String::new();
for p in flagged { for p in &flagged {
list.push_str(&format!("\n - {}", p.display())); list.push_str(&format!("\n - {}", p.display()));
} }
crate::logging::log_note(
&format!(
"AUDIT: world-writable scan FAILED; checked={checked}; duration_ms={elapsed_ms}; flagged:{}",
list
),
Some(cwd),
);
let mut list_err = String::new();
for p in flagged {
list_err.push_str(&format!("\n - {}", p.display()));
}
return Err(anyhow!( return Err(anyhow!(
"Refusing to run: found directories writable by Everyone: {}", "Refusing to run: found directories writable by Everyone: {}",
list list_err
)); ));
} }
// Log success once if nothing flagged
crate::logging::log_note(
&format!(
"AUDIT: world-writable scan OK; checked={checked}; duration_ms={elapsed_ms}"
),
Some(cwd),
);
Ok(()) Ok(())
} }
// Fast mask-based check: does the DACL contain any ACCESS_ALLOWED ACE for
// Everyone that includes generic or specific write bits? Skips inherit-only
// ACEs (do not apply to the current object).
unsafe fn dacl_quick_world_write_mask_allows(p_dacl: *mut ACL, psid_world: *mut c_void) -> bool {
if p_dacl.is_null() {
return false;
}
const INHERIT_ONLY_ACE: u8 = 0x08;
let mut info: ACL_SIZE_INFORMATION = std::mem::zeroed();
let ok = GetAclInformation(
p_dacl as *const ACL,
&mut info as *mut _ as *mut c_void,
std::mem::size_of::<ACL_SIZE_INFORMATION>() as u32,
AclSizeInformation,
);
if ok == 0 {
return false;
}
for i in 0..(info.AceCount as usize) {
let mut p_ace: *mut c_void = std::ptr::null_mut();
if GetAce(p_dacl as *const ACL, i as u32, &mut p_ace) == 0 {
continue;
}
let hdr = &*(p_ace as *const ACE_HEADER);
if hdr.AceType != 0 { // ACCESS_ALLOWED_ACE_TYPE
continue;
}
if (hdr.AceFlags & INHERIT_ONLY_ACE) != 0 {
continue;
}
let base = p_ace as usize;
let sid_ptr = (base
+ std::mem::size_of::<ACE_HEADER>()
+ std::mem::size_of::<u32>()) as *mut c_void; // skip header + mask
if EqualSid(sid_ptr, psid_world) != 0 {
let ace = &*(p_ace as *const ACCESS_ALLOWED_ACE);
let mask = ace.Mask;
let writey = FILE_GENERIC_WRITE
| FILE_WRITE_DATA
| FILE_APPEND_DATA
| FILE_WRITE_EA
| FILE_WRITE_ATTRIBUTES
| GENERIC_WRITE_MASK
| GENERIC_ALL_MASK;
if (mask & writey) != 0 {
return true;
}
}
}
false
}

View File

@@ -55,3 +55,8 @@ pub fn debug_log(msg: &str, base_dir: Option<&Path>) {
eprintln!("{msg}"); eprintln!("{msg}");
} }
} }
// Unconditional note logging to sandbox_commands.rust.log
pub fn log_note(msg: &str, base_dir: Option<&Path>) {
append_line(msg, base_dir);
}