diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..45fa515 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,61 @@ +# 安全策略 / Security Policy + +感谢你关注 U-Claw 的安全性。本文档说明如何向我们报告安全漏洞,以及我们当前支持的版本。 + +## 报告漏洞 / Reporting a Vulnerability + +**请不要在公开 issue 里发布安全漏洞细节。** + +如果你发现了 U-Claw 的安全问题,请通过以下任一渠道私下联系: + +- 邮件:`hefangsheng@u-claw.org`(建议附 PoC、影响范围、复现步骤) +- GitHub 私密漏洞报告:在 [u-claw 仓库](https://github.com/dongsheng123132/u-claw) 的 Security 标签页 → "Report a vulnerability" + +我们会在 **3 个工作日内** 确认收到,并在 **14 天内** 给出初步评估。请允许我们在公开披露前修复问题。 + +### 报告内容建议 + +- 受影响的模块(portable / install / bootable / u-claw-app) +- 受影响的版本或 commit hash +- 触发条件、攻击面(本地 / 局域网 / 互联网) +- 复现步骤或 PoC 代码 +- 你建议的缓解或修复方向 + +## 支持的版本 / Supported Versions + +| 模块 | 版本 | 状态 | +|------|------|------| +| `portable/` | 当前 main | ✅ 接受报告 | +| `install/` (`install.sh` / `install.ps1`) | 当前 main | ✅ 接受报告 | +| `bootable/` (Linux U 盘) | 当前 main | ✅ 接受报告 | +| `u-claw-app/` Electron | 当前 main | ✅ 接受报告 | +| 历史 release tag | — | ⚠️ 仅做严重等级评估,不一定回滚补丁 | + +我们暂未发布稳定版本号,所有修复直接在 main 分支推送。 + +## 范围 / In-Scope vs Out-of-Scope + +### 在范围内(请报告) + +- U-Claw 自身脚本中的命令注入、路径穿越、任意写入、代码执行 +- `data/.openclaw/openclaw.json` 配置加载/写入逻辑中的注入 +- 启动脚本(`Mac-Start.command` / `Windows-Start.bat` / `start.sh`) + 在恶意目录名/环境变量下被劫持的可能 +- 一键安装 (`curl | bash` / `irm | iex`) 链路上的 MITM 风险 +- Bootable USB 制作脚本生成的产物在 Linux Live 环境下的提权问题 + +### 不在范围内 + +- 上游依赖(Node.js / OpenClaw / Electron / Ventoy / Ubuntu)的漏洞 — + 请直接报给上游项目;我们只跟踪并升级版本。 +- 用户主动把自己的 API Key 写到公开仓库 / 截图泄露,不属于本项目缺陷。 +- 物理接触 USB 后的所有攻击(含偷换 USB、键盘记录等),属于硬件安全场景。 +- 在用户已经获得 admin/root 权限的情况下能做的进一步动作。 + +## 致谢 / Acknowledgements + +发现并负责任披露安全问题的研究者,将在修复发布的 commit message 和 release notes 中署名感谢(除非你要求匿名)。 + +--- + +> 本策略借鉴了 GitHub 推荐的开源安全披露格式,未来会随项目成熟度更新。 diff --git a/bootable/3-create-persistence.ps1 b/bootable/3-create-persistence.ps1 index 0590bb3..6c6fc49 100644 --- a/bootable/3-create-persistence.ps1 +++ b/bootable/3-create-persistence.ps1 @@ -73,10 +73,17 @@ if ($wslDistro) { $sizeMB = $sizeGB * 1024 $tmpFile = "/tmp/uclaw_persistence.dat" - wsl -d $wslDistro -- sh -c "rm -f $tmpFile; dd if=/dev/zero of=$tmpFile bs=1M count=0 seek=$sizeMB 2>/dev/null; mkfs.ext4 -F -L casper-rw $tmpFile 2>/dev/null; echo DONE" + # Chain with && so a failed dd aborts before mkfs; capture marker on success only. + $wslOutput = wsl -d $wslDistro -- sh -c "rm -f $tmpFile && dd if=/dev/zero of=$tmpFile bs=1M count=0 seek=$sizeMB 2>/dev/null && mkfs.ext4 -F -L casper-rw $tmpFile >/dev/null 2>&1 && echo DONE" + if ($wslOutput -notmatch "DONE") { + Write-Host "[ERROR] WSL failed to create/format ext4 image (dd or mkfs.ext4 returned non-zero)." -ForegroundColor Red + wsl -d $wslDistro -- rm -f $tmpFile 2>$null + Read-Host "Press Enter to exit" + exit 1 + } - # Copy out via \\wsl$ - $wslNetPath = "\\wsl$\$wslDistro\tmp\uclaw_persistence.dat" + # Copy out via \\wsl$ — use ${wslDistro} so PowerShell does not greedily extend the variable name into the path segment. + $wslNetPath = "\\wsl`$\${wslDistro}\tmp\uclaw_persistence.dat" if (Test-Path $wslNetPath) { Write-Host "[INFO] Copying from WSL to cache..." -ForegroundColor Yellow Copy-Item -Path $wslNetPath -Destination $PersistencePath -Force @@ -132,15 +139,11 @@ if ($wslDistro) { Write-Host "[INFO] Found docker-desktop WSL with mkfs.ext4, creating ext4 image..." -ForegroundColor Green $tmpFile = "/tmp/uclaw_persistence.dat" - # Create sparse file and format in docker-desktop WSL - wsl -d $dockerWSL -- dd if=/dev/zero of=$tmpFile bs=1M count=0 seek=$sizeMB 2>$null - wsl -d $dockerWSL -- /sbin/mkfs.ext4 -F -L casper-rw $tmpFile 2>$null + # Create sparse file and format in docker-desktop WSL — chain with && so a dd failure aborts before mkfs/cp. + $dockerOutput = wsl -d $dockerWSL -- sh -c "rm -f $tmpFile && dd if=/dev/zero of=$tmpFile bs=1M count=0 seek=$sizeMB 2>/dev/null && /sbin/mkfs.ext4 -F -L casper-rw $tmpFile >/dev/null 2>&1 && cp $tmpFile /mnt/host/c/uclaw_persistence_tmp.dat && echo DONE" - # Copy out via /mnt/host/c/ (docker-desktop mounts C: there) $tmpHostPath = "C:\uclaw_persistence_tmp.dat" - wsl -d $dockerWSL -- cp $tmpFile /mnt/host/c/uclaw_persistence_tmp.dat 2>$null - - if (Test-Path $tmpHostPath) { + if (($dockerOutput -match "DONE") -and (Test-Path $tmpHostPath)) { Move-Item -Path $tmpHostPath -Destination $PersistencePath -Force wsl -d $dockerWSL -- rm -f $tmpFile 2>$null $formatted = $true diff --git a/bootable/linux-setup/setup-openclaw.sh b/bootable/linux-setup/setup-openclaw.sh index 44e05d0..1212ad6 100644 --- a/bootable/linux-setup/setup-openclaw.sh +++ b/bootable/linux-setup/setup-openclaw.sh @@ -62,20 +62,29 @@ if [[ -x "$NODE_DIR/bin/node" ]]; then fi if [[ ! -x "$NODE_DIR/bin/node" ]]; then - TMPFILE=$(mktemp /tmp/node-XXXXXX.tar.xz) - # Try China mirror first, then official + if ! TMPFILE=$(mktemp /tmp/node-XXXXXX.tar.xz); then + echo "[ERROR] mktemp failed (cannot create temp file in /tmp). Disk full?" + exit 1 + fi + # Ensure partial downloads are cleaned up no matter how the script exits. + trap 'rm -f "$TMPFILE"' EXIT + + # Try China mirror first, then official. On failure, drop the partial file before retrying. if curl -fSL --connect-timeout 10 -o "$TMPFILE" "${NODE_MIRROR}/${NODE_VERSION}/${NODE_ARCHIVE}" 2>/dev/null; then echo " Downloaded from China mirror." - elif curl -fSL --connect-timeout 10 -o "$TMPFILE" "${NODE_OFFICIAL}/${NODE_VERSION}/${NODE_ARCHIVE}" 2>/dev/null; then - echo " Downloaded from official mirror." else - echo "[ERROR] Failed to download Node.js. Please check your network." rm -f "$TMPFILE" - exit 1 + if curl -fSL --connect-timeout 10 -o "$TMPFILE" "${NODE_OFFICIAL}/${NODE_VERSION}/${NODE_ARCHIVE}" 2>/dev/null; then + echo " Downloaded from official mirror." + else + echo "[ERROR] Failed to download Node.js. Please check your network." + exit 1 + fi fi mkdir -p "$NODE_DIR" tar -xJf "$TMPFILE" --strip-components=1 -C "$NODE_DIR" rm -f "$TMPFILE" + trap - EXIT echo " Node.js extracted to $NODE_DIR" fi @@ -196,7 +205,10 @@ if [[ "$INSTALL_SSH" =~ ^[Yy]$ ]]; then echo "" echo " Set a password for SSH login (user: $REAL_USER):" passwd "$REAL_USER" - LOCAL_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+\.\d+\.\d+\.\d+' | head -1) + # `set -o pipefail` makes the pipeline fail when grep finds no match, which would abort the whole script + # on a host with no non-loopback NIC. Tolerate that case and just leave LOCAL_IP empty. + LOCAL_IP=$(ip -4 addr show 2>/dev/null | grep -oP '(?<=inet\s)(?!127\.)\d+\.\d+\.\d+\.\d+' | head -1 || true) + [[ -z "$LOCAL_IP" ]] && LOCAL_IP="" echo "" echo " SSH enabled! Connect from another computer:" echo " ssh ${REAL_USER}@${LOCAL_IP}" @@ -226,10 +238,10 @@ if [[ -f "$SCRIPT_DIR/test-installation.sh" ]]; then echo "" fi -# 运行快速测试 +# 运行快速测试 — 主流程已 `set -o pipefail`,test 脚本失败不应让整个 setup 退出 (我们已经走完安装). echo "Running quick installation test..." if [[ -f "$SCRIPT_DIR/test-installation.sh" ]]; then - bash "$SCRIPT_DIR/test-installation.sh" | tail -20 + bash "$SCRIPT_DIR/test-installation.sh" 2>&1 | tail -20 || true fi echo " First time? Configure your AI model in the browser after startup." echo "============================================" diff --git a/install/install.sh b/install/install.sh index 0945df4..85930d4 100755 --- a/install/install.sh +++ b/install/install.sh @@ -6,6 +6,9 @@ # ============================================================ set -e +set -o pipefail +# 注意:未启用 `set -u` —— 第 5 步交互式分支会有意把 API_KEY/BASE_URL/KEY_LABEL 等未赋值的变量 +# 用 [-z "$X"] 检测后再写配置;启用 -u 会破坏这条降级路径。改用显式 `: "${VAR:=}"` 默认值过于侵入。 # ---- 颜色定义 ---- GREEN='\033[0;32m' @@ -186,8 +189,16 @@ PKGJSON fi echo -e " ${CYAN}↓${NC} 从国内镜像安装..." - run_npm install --prefix "$CORE_DIR" --registry="$MIRROR" 2>&1 | tail -5 - echo -e " ${GREEN}✓${NC} OpenClaw 安装完成" + NPM_LOG=$(mktemp /tmp/uclaw-npm.XXXXXX.log) + if run_npm install --prefix "$CORE_DIR" --registry="$MIRROR" >"$NPM_LOG" 2>&1; then + tail -5 "$NPM_LOG" + rm -f "$NPM_LOG" + echo -e " ${GREEN}✓${NC} OpenClaw 安装完成" + else + echo -e " ${RED}✗ OpenClaw 安装失败,完整日志见 $NPM_LOG${NC}" + tail -20 "$NPM_LOG" + exit 1 + fi fi echo "" @@ -889,6 +900,11 @@ else echo -e " ${CYAN}$KEY_HINT${NC}" fi + # 转义 API_KEY 中可能破坏 JSON 的字符 (反斜杠和双引号)。这里只处理这两个, + # 因为合法的 API key 极少包含控制字符。如果用户粘贴了奇怪的内容,至少 JSON 仍然可解析。 + API_KEY_JSON=${API_KEY//\\/\\\\} + API_KEY_JSON=${API_KEY_JSON//\"/\\\"} + # 写配置文件 if [ "$PROVIDER" = "custom" ] && [ -n "$BASE_URL" ]; then cat > "$CONFIG_PATH" << CFGEOF @@ -902,7 +918,7 @@ else "providers": { "custom": { "baseUrl": "$BASE_URL", - "apiKey": "$API_KEY", + "apiKey": "$API_KEY_JSON", "api": "openai-completions", "models": [{ "id": "$MODEL_NAME" }] } @@ -922,7 +938,7 @@ CFGEOF "mode": "merge", "providers": { "anthropic": { - "apiKey": "$API_KEY", + "apiKey": "$API_KEY_JSON", "api": "anthropic", "models": [{ "id": "$MODEL_NAME" }] } @@ -942,7 +958,7 @@ CFGEOF "mode": "merge", "providers": { "openai": { - "apiKey": "$API_KEY", + "apiKey": "$API_KEY_JSON", "api": "openai-completions", "models": [{ "id": "$MODEL_NAME" }] }