mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
security: 修复命令注入/路径遍历漏洞 + 收紧 allowedOrigins + 审计日志 + OnceLock 缓存
This commit is contained in:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -5,6 +5,20 @@
|
||||
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
||||
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||
|
||||
## [0.5.6] - 2026-03-06
|
||||
|
||||
### 安全修复 (Security)
|
||||
|
||||
- **dev-api.js 命令注入漏洞** — `search_log` 的 `query` 参数直接拼入 `grep` shell 命令,可注入任意系统命令。改为纯 JS 字符串匹配实现
|
||||
- **dev-api.js 路径遍历漏洞** — `read_memory_file` / `write_memory_file` / `delete_memory_file` 未校验路径,可通过 `../` 读写任意文件。新增 `isUnsafePath()` 检查(与 Rust 端 `memory.rs` 对齐)
|
||||
- **Gateway allowedOrigins 过于宽松** — `patch_gateway_origins()` 设置 `["*"]` 允许任何网页连接本地 Gateway WebSocket。收紧为仅允许 Tauri origin + `localhost:1420`
|
||||
|
||||
### 改进 (Improvements)
|
||||
|
||||
- **AI 助手审计日志** — `assistant_exec` / `assistant_read_file` / `assistant_write_file` 新增操作审计日志,记录到 `~/.openclaw/logs/assistant-audit.log`
|
||||
- **connect frame 版本号** — `device.rs` 中 `userAgent` 和 `client.version` 从硬编码 `1.0.0` 改为编译时读取 `Cargo.toml` 版本
|
||||
- **enhanced_path() 性能优化** — 使用 `OnceLock` 缓存结果,避免每次调用都扫描文件系统
|
||||
|
||||
## [0.5.5] - 2026-03-06
|
||||
|
||||
### 修复 (Bug Fixes)
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"description": "OpenClaw AI Agent 可视化管理面板,基于 Tauri v2 的跨平台桌面应用。支持仪表盘监控、多模型配置、实时 AI 聊天、记忆管理、Agent 管理、网关配置、内网穿透等功能。",
|
||||
"url": "https://claw.qt.cool/",
|
||||
"downloadUrl": "https://github.com/qingchencloud/clawpanel/releases/latest",
|
||||
"softwareVersion": "0.5.5",
|
||||
"softwareVersion": "0.5.6",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "晴辰云 QingchenCloud",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawpanel",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.6",
|
||||
"private": true,
|
||||
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
|
||||
"type": "module",
|
||||
|
||||
@@ -22,6 +22,10 @@ const isMac = process.platform === 'darwin'
|
||||
const isLinux = process.platform === 'linux'
|
||||
const SCOPES = ['operator.admin', 'operator.approvals', 'operator.pairing', 'operator.read', 'operator.write']
|
||||
|
||||
function isUnsafePath(p) {
|
||||
return !p || p.includes('..') || p.includes('\0') || path.isAbsolute(p)
|
||||
}
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve) => {
|
||||
let body = ''
|
||||
@@ -584,12 +588,11 @@ const handlers = {
|
||||
const file = logFiles[logName] || logFiles['gateway']
|
||||
const logPath = path.join(LOGS_DIR, file)
|
||||
if (!fs.existsSync(logPath)) return []
|
||||
try {
|
||||
const output = execSync(`grep -i "${query.replace(/"/g, '\\"')}" "${logPath}" | tail -${maxResults} 2>&1`).toString()
|
||||
return output.split('\n').filter(Boolean)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
// 纯 JS 实现,避免 shell 命令注入
|
||||
const content = fs.readFileSync(logPath, 'utf8')
|
||||
const queryLower = (query || '').toLowerCase()
|
||||
const matched = content.split('\n').filter(line => line.toLowerCase().includes(queryLower))
|
||||
return matched.slice(-maxResults)
|
||||
},
|
||||
|
||||
// Agent 管理
|
||||
@@ -619,6 +622,7 @@ const handlers = {
|
||||
},
|
||||
|
||||
read_memory_file({ path: filePath, agent_id }) {
|
||||
if (isUnsafePath(filePath)) throw new Error('非法路径')
|
||||
const suffix = agent_id && agent_id !== 'main' ? `/agents/${agent_id}` : ''
|
||||
const full = path.join(OPENCLAW_DIR, 'workspace' + suffix, filePath)
|
||||
if (!fs.existsSync(full)) return ''
|
||||
@@ -626,6 +630,7 @@ const handlers = {
|
||||
},
|
||||
|
||||
write_memory_file({ path: filePath, content, category, agent_id }) {
|
||||
if (isUnsafePath(filePath)) throw new Error('非法路径')
|
||||
const suffix = agent_id && agent_id !== 'main' ? `/agents/${agent_id}` : ''
|
||||
const full = path.join(OPENCLAW_DIR, 'workspace' + suffix, filePath)
|
||||
const dir = path.dirname(full)
|
||||
@@ -635,6 +640,7 @@ const handlers = {
|
||||
},
|
||||
|
||||
delete_memory_file({ path: filePath, agent_id }) {
|
||||
if (isUnsafePath(filePath)) throw new Error('非法路径')
|
||||
const suffix = agent_id && agent_id !== 'main' ? `/agents/${agent_id}` : ''
|
||||
const full = path.join(OPENCLAW_DIR, 'workspace' + suffix, filePath)
|
||||
if (fs.existsSync(full)) fs.unlinkSync(full)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clawpanel"
|
||||
version = "0.5.5"
|
||||
version = "0.5.6"
|
||||
edition = "2021"
|
||||
description = "ClawPanel - OpenClaw 可视化管理面板"
|
||||
authors = ["qingchencloud"]
|
||||
|
||||
@@ -4,6 +4,20 @@ use base64::{engine::general_purpose, Engine as _};
|
||||
/// 仅在用户主动开启工具后由 AI 调用
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 审计日志:记录 AI 助手的敏感操作(exec / read / write)
|
||||
fn audit_log(action: &str, detail: &str) {
|
||||
let log_dir = super::openclaw_dir().join("logs");
|
||||
let _ = std::fs::create_dir_all(&log_dir);
|
||||
let log_path = log_dir.join("assistant-audit.log");
|
||||
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||
let line = format!("[{ts}] [{action}] {detail}\n");
|
||||
let _ = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_path)
|
||||
.and_then(|mut f| std::io::Write::write_all(&mut f, line.as_bytes()));
|
||||
}
|
||||
|
||||
/// ClawPanel 数据目录(~/.openclaw/clawpanel/)
|
||||
fn data_dir() -> PathBuf {
|
||||
super::openclaw_dir().join("clawpanel")
|
||||
@@ -125,6 +139,8 @@ pub async fn assistant_exec(command: String, cwd: Option<String>) -> Result<Stri
|
||||
.to_string()
|
||||
});
|
||||
|
||||
audit_log("EXEC", &format!("cmd={command} cwd={work_dir}"));
|
||||
|
||||
let output;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -184,6 +200,7 @@ pub async fn assistant_exec(command: String, cwd: Option<String>) -> Result<Stri
|
||||
/// 读取文件内容
|
||||
#[tauri::command]
|
||||
pub async fn assistant_read_file(path: String) -> Result<String, String> {
|
||||
audit_log("READ", &path);
|
||||
let content = tokio::fs::read_to_string(&path)
|
||||
.await
|
||||
.map_err(|e| format!("读取文件失败 {path}: {e}"))?;
|
||||
@@ -202,6 +219,7 @@ pub async fn assistant_read_file(path: String) -> Result<String, String> {
|
||||
/// 写入文件
|
||||
#[tauri::command]
|
||||
pub async fn assistant_write_file(path: String, content: String) -> Result<String, String> {
|
||||
audit_log("WRITE", &format!("{path} ({} bytes)", content.len()));
|
||||
if let Some(parent) = PathBuf::from(&path).parent() {
|
||||
tokio::fs::create_dir_all(parent)
|
||||
.await
|
||||
|
||||
@@ -119,7 +119,7 @@ pub fn create_connect_frame(nonce: String, gateway_token: String) -> Result<Valu
|
||||
"maxProtocol": 3,
|
||||
"client": {
|
||||
"id": "openclaw-control-ui",
|
||||
"version": "1.0.0",
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"platform": platform,
|
||||
"deviceFamily": device_family,
|
||||
"mode": "ui"
|
||||
@@ -136,7 +136,7 @@ pub fn create_connect_frame(nonce: String, gateway_token: String) -> Result<Valu
|
||||
"signature": sig_b64,
|
||||
},
|
||||
"locale": "zh-CN",
|
||||
"userAgent": "ClawPanel/1.0.0",
|
||||
"userAgent": format!("ClawPanel/{}", env!("CARGO_PKG_VERSION")),
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub mod agent;
|
||||
pub mod assistant;
|
||||
@@ -15,12 +16,19 @@ pub fn openclaw_dir() -> PathBuf {
|
||||
dirs::home_dir().unwrap_or_default().join(".openclaw")
|
||||
}
|
||||
|
||||
/// 缓存 enhanced_path 结果,避免每次调用都扫描文件系统
|
||||
static ENHANCED_PATH_CACHE: OnceLock<String> = OnceLock::new();
|
||||
|
||||
/// Tauri 应用启动时 PATH 可能不完整:
|
||||
/// - macOS 从 Finder 启动时 PATH 只有 /usr/bin:/bin:/usr/sbin:/sbin
|
||||
/// - Windows 上安装 Node.js 到非默认路径、或安装后未重启进程
|
||||
///
|
||||
/// 补充 Node.js / npm 常见安装路径
|
||||
pub fn enhanced_path() -> String {
|
||||
ENHANCED_PATH_CACHE.get_or_init(build_enhanced_path).clone()
|
||||
}
|
||||
|
||||
fn build_enhanced_path() -> String {
|
||||
let current = std::env::var("PATH").unwrap_or_default();
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
|
||||
|
||||
@@ -115,8 +115,14 @@ fn patch_gateway_origins() {
|
||||
return;
|
||||
};
|
||||
|
||||
// 放行全部 origin,确保 Tauri 正式/开发模式、Web 模式都能连接
|
||||
let origins = serde_json::json!(["*"]);
|
||||
// 仅允许 Tauri 应用 + 本地开发服务器的 origin
|
||||
let origins = serde_json::json!([
|
||||
"tauri://localhost",
|
||||
"https://tauri.localhost",
|
||||
"http://tauri.localhost",
|
||||
"http://localhost:1420",
|
||||
"http://127.0.0.1:1420"
|
||||
]);
|
||||
|
||||
if let Some(obj) = config.as_object_mut() {
|
||||
let gateway = obj
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
|
||||
"productName": "ClawPanel",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.6",
|
||||
"identifier": "ai.openclaw.clawpanel",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
Reference in New Issue
Block a user