Files
zeroclaw/src/security/mod.rs
T
simianastronaut 6cdc92a256 fix(agent): use char-boundary-safe slicing to prevent CJK text panic
Replace unsafe byte-index string slicing (`&text[..N]`) with
char-boundary-safe alternatives in memory consolidation and security
redaction to prevent panics when multi-byte UTF-8 characters (e.g.
Chinese/Japanese/Korean) span the slice boundary.

Fixes the same class of bug as the prior fix in `execute_one_tool`
(commit 8fcbb6eb), applied to two remaining instances:
- `src/memory/consolidation.rs`: truncation at byte 4000 and 200
- `src/security/mod.rs`: `redact()` prefix at byte 4

Adds regression tests with CJK input for both locations.

Closes #3533

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:27:09 -04:00

121 lines
3.9 KiB
Rust

//! Security subsystem for policy enforcement, sandboxing, and secret management.
//!
//! This module provides the security infrastructure for ZeroClaw. The core type
//! [`SecurityPolicy`] defines autonomy levels, workspace boundaries, and
//! access-control rules that are enforced across the tool and runtime subsystems.
//! [`PairingGuard`] implements device pairing for channel authentication, and
//! [`SecretStore`] handles encrypted credential storage.
//!
//! OS-level isolation is provided through the [`Sandbox`] trait defined in
//! [`traits`], with pluggable backends including Docker, Firejail, Bubblewrap,
//! and Landlock. The [`create_sandbox`] function selects the best available
//! backend at runtime. An [`AuditLogger`] records security-relevant events for
//! forensic review.
//!
//! # Extension
//!
//! To add a new sandbox backend, implement [`Sandbox`] in a new submodule and
//! register it in [`detect::create_sandbox`]. See `AGENTS.md` §7.5 for security
//! change guidelines.
pub mod audit;
#[cfg(feature = "sandbox-bubblewrap")]
pub mod bubblewrap;
pub mod detect;
pub mod docker;
// Prompt injection defense (contributed from RustyClaw, MIT licensed)
pub mod domain_matcher;
pub mod estop;
#[cfg(target_os = "linux")]
pub mod firejail;
#[cfg(feature = "sandbox-landlock")]
pub mod landlock;
pub mod leak_detector;
pub mod otp;
pub mod pairing;
pub mod policy;
pub mod prompt_guard;
pub mod secrets;
pub mod traits;
#[allow(unused_imports)]
pub use audit::{AuditEvent, AuditEventType, AuditLogger};
#[allow(unused_imports)]
pub use detect::create_sandbox;
pub use domain_matcher::DomainMatcher;
#[allow(unused_imports)]
pub use estop::{EstopLevel, EstopManager, EstopState, ResumeSelector};
#[allow(unused_imports)]
pub use otp::OtpValidator;
#[allow(unused_imports)]
pub use pairing::PairingGuard;
pub use policy::{AutonomyLevel, SecurityPolicy};
#[allow(unused_imports)]
pub use secrets::SecretStore;
#[allow(unused_imports)]
pub use traits::{NoopSandbox, Sandbox};
// Prompt injection defense exports
#[allow(unused_imports)]
pub use leak_detector::{LeakDetector, LeakResult};
#[allow(unused_imports)]
pub use prompt_guard::{GuardAction, GuardResult, PromptGuard};
/// Redact sensitive values for safe logging. Shows first 4 chars + "***" suffix.
/// This function intentionally breaks the data-flow taint chain for static analysis.
pub fn redact(value: &str) -> String {
if value.len() <= 4 {
"***".to_string()
} else {
// Use char-boundary-safe slicing to prevent panic on multi-byte UTF-8.
let prefix = value
.char_indices()
.nth(4)
.map(|(byte_idx, _)| &value[..byte_idx])
.unwrap_or(value);
format!("{}***", prefix)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reexported_policy_and_pairing_types_are_usable() {
let policy = SecurityPolicy::default();
assert_eq!(policy.autonomy, AutonomyLevel::Supervised);
let guard = PairingGuard::new(false, &[]);
assert!(!guard.require_pairing());
}
#[test]
fn reexported_secret_store_encrypt_decrypt_roundtrip() {
let temp = tempfile::tempdir().unwrap();
let store = SecretStore::new(temp.path(), false);
let encrypted = store.encrypt("top-secret").unwrap();
let decrypted = store.decrypt(&encrypted).unwrap();
assert_eq!(decrypted, "top-secret");
}
#[test]
fn redact_hides_most_of_value() {
assert_eq!(redact("abcdefgh"), "abcd***");
assert_eq!(redact("ab"), "***");
assert_eq!(redact(""), "***");
assert_eq!(redact("12345"), "1234***");
}
#[test]
fn redact_handles_multibyte_utf8_without_panic() {
// CJK characters are 3 bytes each; slicing at byte 4 would panic
// without char-boundary-safe handling.
let result = redact("密码是很长的秘密");
assert!(result.ends_with("***"));
assert!(result.is_char_boundary(result.len()));
}
}