fix(bootable+install): harden persistence script, setup-openclaw, install.sh + add SECURITY.md

- 3-create-persistence.ps1: chain dd && mkfs.ext4 with && (was ;, so dd
  failures still ran mkfs and produced bogus images); fix \wsl$\$wslDistro
  variable expansion ambiguity using \${wslDistro}; mirror docker-desktop
  branch with the same && chaining
- setup-openclaw.sh: guard mktemp failure; ensure partial node tarballs are
  cleaned via EXIT trap; tolerate hosts with no non-loopback NIC under
  pipefail; don't let test-installation.sh failure exit the post-install
- install.sh: add `set -o pipefail`; surface npm install failures (was
  hidden by `| tail -5`); JSON-escape user-provided API_KEY before writing
  to openclaw.json so keys with backslashes/quotes don't break the config
- Add SECURITY.md: private disclosure channel + scope
This commit is contained in:
hfshfg
2026-05-01 15:45:32 +08:00
parent b0a9cb3f58
commit 1bf5402d34
4 changed files with 116 additions and 24 deletions

61
SECURITY.md Normal file
View File

@@ -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 推荐的开源安全披露格式,未来会随项目成熟度更新。

View File

@@ -73,10 +73,17 @@ if ($wslDistro) {
$sizeMB = $sizeGB * 1024 $sizeMB = $sizeGB * 1024
$tmpFile = "/tmp/uclaw_persistence.dat" $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$ # 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" $wslNetPath = "\\wsl`$\${wslDistro}\tmp\uclaw_persistence.dat"
if (Test-Path $wslNetPath) { if (Test-Path $wslNetPath) {
Write-Host "[INFO] Copying from WSL to cache..." -ForegroundColor Yellow Write-Host "[INFO] Copying from WSL to cache..." -ForegroundColor Yellow
Copy-Item -Path $wslNetPath -Destination $PersistencePath -Force 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 Write-Host "[INFO] Found docker-desktop WSL with mkfs.ext4, creating ext4 image..." -ForegroundColor Green
$tmpFile = "/tmp/uclaw_persistence.dat" $tmpFile = "/tmp/uclaw_persistence.dat"
# Create sparse file and format in docker-desktop WSL # Create sparse file and format in docker-desktop WSL — chain with && so a dd failure aborts before mkfs/cp.
wsl -d $dockerWSL -- dd if=/dev/zero of=$tmpFile bs=1M count=0 seek=$sizeMB 2>$null $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"
wsl -d $dockerWSL -- /sbin/mkfs.ext4 -F -L casper-rw $tmpFile 2>$null
# Copy out via /mnt/host/c/ (docker-desktop mounts C: there)
$tmpHostPath = "C:\uclaw_persistence_tmp.dat" $tmpHostPath = "C:\uclaw_persistence_tmp.dat"
wsl -d $dockerWSL -- cp $tmpFile /mnt/host/c/uclaw_persistence_tmp.dat 2>$null if (($dockerOutput -match "DONE") -and (Test-Path $tmpHostPath)) {
if (Test-Path $tmpHostPath) {
Move-Item -Path $tmpHostPath -Destination $PersistencePath -Force Move-Item -Path $tmpHostPath -Destination $PersistencePath -Force
wsl -d $dockerWSL -- rm -f $tmpFile 2>$null wsl -d $dockerWSL -- rm -f $tmpFile 2>$null
$formatted = $true $formatted = $true

View File

@@ -62,20 +62,29 @@ if [[ -x "$NODE_DIR/bin/node" ]]; then
fi fi
if [[ ! -x "$NODE_DIR/bin/node" ]]; then if [[ ! -x "$NODE_DIR/bin/node" ]]; then
TMPFILE=$(mktemp /tmp/node-XXXXXX.tar.xz) if ! TMPFILE=$(mktemp /tmp/node-XXXXXX.tar.xz); then
# Try China mirror first, then official 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 if curl -fSL --connect-timeout 10 -o "$TMPFILE" "${NODE_MIRROR}/${NODE_VERSION}/${NODE_ARCHIVE}" 2>/dev/null; then
echo " Downloaded from China mirror." echo " Downloaded from China mirror."
elif curl -fSL --connect-timeout 10 -o "$TMPFILE" "${NODE_OFFICIAL}/${NODE_VERSION}/${NODE_ARCHIVE}" 2>/dev/null; then else
rm -f "$TMPFILE"
if curl -fSL --connect-timeout 10 -o "$TMPFILE" "${NODE_OFFICIAL}/${NODE_VERSION}/${NODE_ARCHIVE}" 2>/dev/null; then
echo " Downloaded from official mirror." echo " Downloaded from official mirror."
else else
echo "[ERROR] Failed to download Node.js. Please check your network." echo "[ERROR] Failed to download Node.js. Please check your network."
rm -f "$TMPFILE"
exit 1 exit 1
fi fi
fi
mkdir -p "$NODE_DIR" mkdir -p "$NODE_DIR"
tar -xJf "$TMPFILE" --strip-components=1 -C "$NODE_DIR" tar -xJf "$TMPFILE" --strip-components=1 -C "$NODE_DIR"
rm -f "$TMPFILE" rm -f "$TMPFILE"
trap - EXIT
echo " Node.js extracted to $NODE_DIR" echo " Node.js extracted to $NODE_DIR"
fi fi
@@ -196,7 +205,10 @@ if [[ "$INSTALL_SSH" =~ ^[Yy]$ ]]; then
echo "" echo ""
echo " Set a password for SSH login (user: $REAL_USER):" echo " Set a password for SSH login (user: $REAL_USER):"
passwd "$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="<your-ip>"
echo "" echo ""
echo " SSH enabled! Connect from another computer:" echo " SSH enabled! Connect from another computer:"
echo " ssh ${REAL_USER}@${LOCAL_IP}" echo " ssh ${REAL_USER}@${LOCAL_IP}"
@@ -226,10 +238,10 @@ if [[ -f "$SCRIPT_DIR/test-installation.sh" ]]; then
echo "" echo ""
fi fi
# 运行快速测试 # 运行快速测试 — 主流程已 `set -o pipefail`,test 脚本失败不应让整个 setup 退出 (我们已经走完安装).
echo "Running quick installation test..." echo "Running quick installation test..."
if [[ -f "$SCRIPT_DIR/test-installation.sh" ]]; then 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 fi
echo " First time? Configure your AI model in the browser after startup." echo " First time? Configure your AI model in the browser after startup."
echo "============================================" echo "============================================"

View File

@@ -6,6 +6,9 @@
# ============================================================ # ============================================================
set -e set -e
set -o pipefail
# 注意:未启用 `set -u` —— 第 5 步交互式分支会有意把 API_KEY/BASE_URL/KEY_LABEL 等未赋值的变量
# 用 [-z "$X"] 检测后再写配置;启用 -u 会破坏这条降级路径。改用显式 `: "${VAR:=}"` 默认值过于侵入。
# ---- 颜色定义 ---- # ---- 颜色定义 ----
GREEN='\033[0;32m' GREEN='\033[0;32m'
@@ -186,8 +189,16 @@ PKGJSON
fi fi
echo -e " ${CYAN}${NC} 从国内镜像安装..." echo -e " ${CYAN}${NC} 从国内镜像安装..."
run_npm install --prefix "$CORE_DIR" --registry="$MIRROR" 2>&1 | tail -5 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 安装完成" echo -e " ${GREEN}${NC} OpenClaw 安装完成"
else
echo -e " ${RED}✗ OpenClaw 安装失败,完整日志见 $NPM_LOG${NC}"
tail -20 "$NPM_LOG"
exit 1
fi
fi fi
echo "" echo ""
@@ -889,6 +900,11 @@ else
echo -e " ${CYAN}$KEY_HINT${NC}" echo -e " ${CYAN}$KEY_HINT${NC}"
fi 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 if [ "$PROVIDER" = "custom" ] && [ -n "$BASE_URL" ]; then
cat > "$CONFIG_PATH" << CFGEOF cat > "$CONFIG_PATH" << CFGEOF
@@ -902,7 +918,7 @@ else
"providers": { "providers": {
"custom": { "custom": {
"baseUrl": "$BASE_URL", "baseUrl": "$BASE_URL",
"apiKey": "$API_KEY", "apiKey": "$API_KEY_JSON",
"api": "openai-completions", "api": "openai-completions",
"models": [{ "id": "$MODEL_NAME" }] "models": [{ "id": "$MODEL_NAME" }]
} }
@@ -922,7 +938,7 @@ CFGEOF
"mode": "merge", "mode": "merge",
"providers": { "providers": {
"anthropic": { "anthropic": {
"apiKey": "$API_KEY", "apiKey": "$API_KEY_JSON",
"api": "anthropic", "api": "anthropic",
"models": [{ "id": "$MODEL_NAME" }] "models": [{ "id": "$MODEL_NAME" }]
} }
@@ -942,7 +958,7 @@ CFGEOF
"mode": "merge", "mode": "merge",
"providers": { "providers": {
"openai": { "openai": {
"apiKey": "$API_KEY", "apiKey": "$API_KEY_JSON",
"api": "openai-completions", "api": "openai-completions",
"models": [{ "id": "$MODEL_NAME" }] "models": [{ "id": "$MODEL_NAME" }]
} }