fix: 深度审计修复 — WASM 安全加固 + A2A 编译路径 + 测试编译
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

CRITICAL:
- zclaw_file_read: 路径遍历修复 — 组件级过滤替代前缀检查
- zclaw_http_fetch: SSRF 防护 — URL scheme 白名单 + 私有IP段阻止
- Phase 4A: 移除 zclaw-protocols a2a feature gate, A2A 始终编译
- 移除 kernel/desktop multi-agent feature (不再控制任何代码)

MEDIUM:
- user_profiler: FactCategory cfg(test) 导入修复 (563 测试全通过)
This commit is contained in:
iven
2026-04-18 09:11:15 +08:00
parent 0522f2bf95
commit 4e4eefdde1
6 changed files with 66 additions and 17 deletions

View File

@@ -8,9 +8,7 @@ rust-version.workspace = true
description = "ZCLAW kernel - central coordinator for all subsystems" description = "ZCLAW kernel - central coordinator for all subsystems"
[features] [features]
default = ["multi-agent"] default = []
# Enable multi-agent orchestration (Director, A2A protocol)
multi-agent = ["zclaw-protocols/a2a"]
[dependencies] [dependencies]
zclaw-types = { workspace = true } zclaw-types = { workspace = true }

View File

@@ -1,20 +1,15 @@
//! ZCLAW Protocols //! ZCLAW Protocols
//! //!
//! Protocol support for MCP (Model Context Protocol) and A2A (Agent-to-Agent). //! Protocol support for MCP (Model Context Protocol) and A2A (Agent-to-Agent).
//!
//! A2A is gated behind the `a2a` feature flag (reserved for future multi-agent scenarios).
//! MCP is always available as a framework for tool integration.
mod mcp; mod mcp;
mod mcp_types; mod mcp_types;
mod mcp_tool_adapter; mod mcp_tool_adapter;
mod mcp_transport; mod mcp_transport;
#[cfg(feature = "a2a")]
mod a2a; mod a2a;
pub use mcp::*; pub use mcp::*;
pub use mcp_types::*; pub use mcp_types::*;
pub use mcp_tool_adapter::*; pub use mcp_tool_adapter::*;
pub use mcp_transport::*; pub use mcp_transport::*;
#[cfg(feature = "a2a")]
pub use a2a::*; pub use a2a::*;

View File

@@ -9,7 +9,7 @@ description = "ZCLAW skill system"
[features] [features]
default = [] default = []
wasm = ["wasmtime", "wasmtime-wasi/p1", "ureq"] wasm = ["wasmtime", "wasmtime-wasi/p1", "ureq", "url"]
[dependencies] [dependencies]
zclaw-types = { workspace = true } zclaw-types = { workspace = true }
@@ -28,3 +28,4 @@ shlex = { workspace = true }
wasmtime = { workspace = true, optional = true } wasmtime = { workspace = true, optional = true }
wasmtime-wasi = { workspace = true, optional = true } wasmtime-wasi = { workspace = true, optional = true }
ureq = { workspace = true, optional = true } ureq = { workspace = true, optional = true }
url = { workspace = true, optional = true }

View File

@@ -267,6 +267,46 @@ fn add_host_functions(linker: &mut Linker<WasiP1Ctx>, network_allowed: bool) ->
return -1; return -1;
} }
// Security: validate URL scheme to prevent SSRF.
// Only http:// and https:// are allowed.
let parsed = match url::Url::parse(&url) {
Ok(u) => u,
Err(_) => {
warn!("[WasmSkill] http_fetch denied — invalid URL: {}", url);
return -1;
}
};
let scheme = parsed.scheme();
if scheme != "http" && scheme != "https" {
warn!("[WasmSkill] http_fetch denied — unsupported scheme: {}", scheme);
return -1;
}
// Block private/loopback hosts to prevent SSRF
if let Some(host) = parsed.host_str() {
let lower = host.to_lowercase();
if lower == "localhost"
|| lower.starts_with("127.")
|| lower.starts_with("10.")
|| lower.starts_with("192.168.")
|| lower.starts_with("169.254.")
|| lower.starts_with("0.")
|| lower.ends_with(".internal")
|| lower.ends_with(".local")
{
warn!("[WasmSkill] http_fetch denied — private/loopback host: {}", host);
return -1;
}
// Also block 172.16.0.0/12 range
if lower.starts_with("172.") {
if let Ok(second) = lower.split('.').nth(1).unwrap_or("0").parse::<u8>() {
if (16..=31).contains(&second) {
warn!("[WasmSkill] http_fetch denied — private host (172.16-31.x.x): {}", host);
return -1;
}
}
}
}
debug!("[WasmSkill] guest http_fetch: {}", url); debug!("[WasmSkill] guest http_fetch: {}", url);
// Synchronous HTTP GET (we're already on a blocking thread) // Synchronous HTTP GET (we're already on a blocking thread)
@@ -309,15 +349,30 @@ fn add_host_functions(linker: &mut Linker<WasiP1Ctx>, network_allowed: bool) ->
return -1; return -1;
} }
// Security: only allow reads under /workspace (preopen root) // Security: validate path stays within /workspace sandbox.
if path.starts_with("..") || path.starts_with('/') { // Reject absolute paths, and filter any path component that
// is ".." (e.g. "foo/../../etc/passwd").
let joined = std::path::Path::new("/workspace").join(&path);
let mut safe = true;
for comp in joined.components() {
match comp {
std::path::Component::ParentDir => {
safe = false;
break;
}
std::path::Component::RootDir | std::path::Component::Prefix(_) => {
safe = false;
break;
}
_ => {} // Normal, CurDir — ok
}
}
if !safe {
warn!("[WasmSkill] guest file_read denied — path escapes sandbox: {}", path); warn!("[WasmSkill] guest file_read denied — path escapes sandbox: {}", path);
return -1; return -1;
} }
let full_path = format!("/workspace/{}", path); match std::fs::read(&joined) {
match std::fs::read(&full_path) {
Ok(data) => write_guest_bytes(&mut caller, out_ptr, out_cap, &data), Ok(data) => write_guest_bytes(&mut caller, out_ptr, out_cap, &data),
Err(e) => { Err(e) => {
debug!("[WasmSkill] file_read error for {}: {}", path, e); debug!("[WasmSkill] file_read error for {}: {}", path, e);

View File

@@ -16,9 +16,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[features] [features]
default = ["multi-agent"] default = []
# Multi-agent orchestration (A2A protocol, Director, agent delegation)
multi-agent = ["zclaw-kernel/multi-agent"]
dev-server = ["dep:axum", "dep:tower-http"] dev-server = ["dep:axum", "dep:tower-http"]
[dependencies] [dependencies]

View File

@@ -10,6 +10,8 @@ use std::sync::Arc;
use chrono::Utc; use chrono::Utc;
use tracing::{debug, warn}; use tracing::{debug, warn};
use zclaw_memory::fact::Fact; use zclaw_memory::fact::Fact;
#[cfg(test)]
use zclaw_memory::fact::FactCategory;
use zclaw_memory::user_profile_store::{ use zclaw_memory::user_profile_store::{
CommStyle, Level, UserProfile, UserProfileStore, CommStyle, Level, UserProfile, UserProfileStore,
}; };