From 8abdf1cc3a4a0f460947da135570ffea66d423d2 Mon Sep 17 00:00:00 2001 From: user Date: Fri, 28 Aug 2026 19:05:23 +0800 Subject: [PATCH] fix(core): restrict session artifact permissions on unix Session JSON artifacts (prompts, outputs, transcripts) are written to a temporary file that inherits the process umask, typically 0o644, and are then renamed into place, so the published file stays world-readable on multi-user hosts. Before publishing the temporary file, tighten its permissions to owner-only (previous mode masked with 0o700) on unix so the renamed artifact ends up 0o600-equivalent regardless of the process umask. Permission tightening is best-effort: failures to read or set the permissions are logged and the write still succeeds. Test: cargo check --locked -p bitfun-services-core --jobs 4 (0 errors, 0 warnings); cargo test --locked -p bitfun-services-core --features local-storage --test storage_owner_contracts --jobs 4 (12 passed). AI: AI-assisted, locally tested (cargo check + contract tests on Windows; unix behavior to be exercised by the CI ubuntu/macos matrix). --- .../services/services-core/src/json_store.rs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/crates/services/services-core/src/json_store.rs b/src/crates/services/services-core/src/json_store.rs index 2cacc4b385..312fe02e7c 100644 --- a/src/crates/services/services-core/src/json_store.rs +++ b/src/crates/services/services-core/src/json_store.rs @@ -306,6 +306,46 @@ impl JsonFileStore { if let Err(source) = fs::write(&tmp_path, &bytes).await { return Err(JsonFileStoreError::WriteTemp { source }); } + // Session artifacts carry full prompt/output content and must not be + // world-readable on multi-user hosts. The temp file inherits the + // process umask by default; force owner-only (0o600-equivalent) on + // unix before the rename publishes it. Best-effort: a + // set_permissions failure is logged, not fatal — the file is still + // written. + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = fs::metadata(&tmp_path) + .await + .map(|metadata| metadata.permissions().mode()); + match mode { + Ok(previous_mode) => { + // Preserve the owner read/write/execute bits and clear + // group/other access so the published file is + // 0o600-equivalent regardless of the process umask. + let restricted = previous_mode & 0o700; + if let Err(error) = fs::set_permissions( + &tmp_path, + std::fs::Permissions::from_mode(restricted), + ) + .await + { + warn!( + "Failed to restrict permissions on temporary file {}: {} (continuing; the file may be readable by other local users)", + tmp_path.display(), + error + ); + } + } + Err(error) => { + warn!( + "Failed to read permissions of temporary file {}: {} (continuing)", + tmp_path.display(), + error + ); + } + } + } let replacement = match policy { AtomicWritePolicy::BestEffortReplace => {