Files
workpod/docs/04-审核/04-Shell脚本质量审核.md

205 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Shell 脚本质量审核报告
> 审核日期:2026-04-07
> 审核范围:WorkPod Alpine(含 Test 变体)全部 Shell 脚本
> 审核人:Shell 脚本专家
---
## 脚本清单
| 脚本 | 行数 | 用途 | 复杂度 |
|------|------|------|--------|
| entrypoint.sh | 81 | Alpine 版容器入口,启动 SSH + ttyd + 开发工具检测 | 中 |
| entrypoint-test.sh | 129 | Test 容器入口,额外创建 developer 用户 + Claude 全权限配置 | 中高 | 已归档(_archive/),非当前活跃 |
| ttyd-session.sh | 63 | tmux 会话管理,URL 参数解析 + 交互式菜单 | 中 |
| **合计** | **273** | | |
---
## 逐脚本审核
### entrypoint.shAlpine 版)
**文件路径:** `E:/wk-lab/workpod/entrypoint.sh`
#### 健壮性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| R1 | **SSHD_PID 未赋值(与 Ubuntu 版相同的问题)** | 高 | `cleanup()``kill "$SSHD_PID"` 因 PID 始终为 0 而跳过。`/usr/sbin/sshd` 在第42行前台启动(无 `&`),实际上 sshd 作为守护进程运行后返回,但未捕获其 PID。应改为 `/usr/sbin/sshd & SSHD_PID=$!` 或在 cleanup 中用 `pkill sshd`。 |
| R2 | **sshd 未使用 `-D` 前台模式** | 中 | 第42行直接调用 `/usr/sbin/sshd`,它会 fork 为守护进程。如果后续命令失败导致脚本退出,sshd 会成为孤儿进程。建议用 `-D` 参数保持前台或正确追踪 PID。 |
| R3 | **sleep 1 硬编码等待时间** | 低 | 与 Ubuntu 版相同,仅等待 1 秒判断 ttyd 存活。慢速系统上可能不够。 |
| R4 | **dev-tools.sh 中的 ls -d 可能匹配多个目录** | 低 | 第38行 `ls -d /root/rust-*/bin` 如果存在多个 rust 版本目录,`head -1` 只取第一个。行为可预测但隐式依赖排序。 |
| R5 | **cleanup 中 wait 无参数** | 低 | 第11行 `wait 2>/dev/null` 等待所有后台子进程。由于只跟踪了 TTYD_PID 和 SSHD_PID(后者实际为 0),行为基本正确但语义不精确。 |
#### 可移植性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| P1 | **shebang 使用 bash 但环境为 Alpine/musl** | 中 | `#!/bin/bash` 要求安装 `bash` 包。Dockerfile 第31行已包含 `bash` 依赖,所以当前可工作。但如果未来精简镜像可能出问题。脚本中未使用任何 bashism(数组、`[[ ]]` 等),理论上可改用 `#!/bin/sh`。 |
| P2 | **pidof 替代 pgrep** | 正面 | 第55行使用 `pidof sshd` 而非 `pgrep -x sshd`,这是正确的 POSIX 兼容选择,在 BusyBox 环境下可靠工作。 |
| P3 | **gvm source 命令** | 低 | dev-tools.sh 第34行 `source /root/gvm/scripts/gvm` 依赖 gvm 的初始化脚本格式。如果 gvm 不存在该路径会静默失败(有 `2>/dev/null`)。 |
#### 安全性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| S1 | **ttyd 密码过于简单 `ada:123`** | **[已改进]** | 原第45行 `-c ada:123` 已改为从 `${TTYD_CREDENTIALS:-jc:1234567}` 环境变量读取,不再硬编码在脚本中。密码强度可通过环境变量灵活配置。 |
| S2 | **硬编码密码明文出现在脚本和日志中** | **[已改进]** | 同上,密码已改为通过环境变量 `TTYD_CREDENTIALS` 传入,不再明文出现在脚本源码中。 |
| S3 | **dev-tools.sh 使用单引号 heredoc(安全)** | 正面 | 第32行 `<< 'PROFILE'` 阻止变量展开,防止注入。这是正确的做法。 |
| S4 | **无用户隔离** | 信息 | Alpine 默认版直接以 root 运行一切,没有像 Test 版那样创建 developer 用户。作为开发环境可接受。 |
#### 代码质量
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| Q1 | **开发工具检测逻辑完善** | 正面 | 覆盖 cargo/rustup/gvm/pyenv/go/rust独立安装/local bin 共 7 种开发工具路径。 |
| Q2 | **双轨 PATH 设置** | 正面 | 既在当前 shell 设置 export(第19-29行),又写入 /etc/profile.d/dev-tools.sh(第32-40行)确保新会话生效。设计合理。 |
| Q3 | **启动信息展示完整** | 正面 | 展示 Node/npm/Claude/Rust/Go/Python 版本 + 连接方式,便于快速确认环境状态。 |
| Q4 | **版本检测容错好** | 正面 | 所有 `--version` 命令都有 `2>/dev/null \|\| echo '未安装'` 保护。 |
| Q5 | **cleanup 函数与 Ubuntu 版完全重复** | 低 | cleanup/trap 模式在三份 entrypoint 中复制粘贴。 |
#### 代码质量评分:**7.5 / 10**
> 扣分项:SSHD_PID 未赋值(-1)、密码过弱(-0.5)、sshd 前台模式(-0.5)、代码重复(-0.5)
---
### entrypoint-test.shTest 容器入口) **[归档]**
> 此文件已移至 `_archive/`,以下问题仅作历史记录。
**文件路径:** `E:/wk-lab/workpod-alpine/entrypoint-test.sh`(已归档至 `_archive/`
#### 健壮性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| R1 | **SSHD_PID 未赋值(继承自 Alpine 版)** | 高 | 同 entrypoint.sh 的 R1 问题。 |
| R2 | **adduser -D 无密码设置** | 中 | 第32行 `adduser -D -s /bin/bash developer` 创建用户时未设置密码。developer 用户只能通过 sudo 操作,不能直接 SSH 登录(除非配置了密钥认证)。这可能是故意的,但应明确注释说明。 |
| R3 | **sudoers 文件写入无原子性保护** | 低 | 第34行直接 `echo ... > /etc/sudoers.d/developer`。如果脚本在中途被中断,可能留下不完整的 sudoers 文件导致 sudo 不可用。建议先写临时文件再 mv。 |
| R4 | **chown -R 递归操作范围大** | 低 | 第37行 `chown -R developer:developer /home/developer` 对整个 home 目录递归修改所有权。如果有其他进程正在向该目录写入文件,可能出现竞态。但在容器启动阶段风险极低。 |
| R5 | **settings.json 双引号 heredoc 安全** | 正面 | 第41行和第51行都使用 `<< 'SETTINGS'` 单引号 heredocJSON 内容不会被 shell 解释。做法正确。 |
#### 可移植性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| P1 | **adduser -D 是 Alpine 特有** | 信息 | `-D` 标志表示"不分配密码",是 BusyBox adduser 的扩展。本脚本专用于 Alpine,无移植需求。 |
| P2 | **bash 依赖同 Alpine 版** | 中 | 同 entrypoint.sh 的 P1 问题。 |
#### 安全性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| S1 | **Claude Code settings.json 全权限 allow:["*"]** | **严重 [归档]** | 第43-47行和第53-57行为 root 和 developer 用户都设置了 `"allow": ["*"]`,意味着 Claude Code 可以执行任意文件读写、命令执行等操作而无需用户确认。虽然这是开发环境的故意设计(沙箱用途),但应在文档中明确标注此安全策略的影响范围。**归档文件中的配置,不影响当前版本。** |
| S2 | **.bashrc 中环境变量传递方式** | 中 | 第67-70行使用双引号 heredoc `<< BASHRC` 并在其中引用 `${ANTHROPIC_AUTH_TOKEN}` 等变量。这里变量会在 heredoc 写入时展开——即使用 entrypoint 进程的环境变量值。这意味着:(1) 如果环境变量未设置,会写入空字符串;(2) 如果值包含特殊字符(如 `$`` `),可能被二次解释。当前用法基本安全但需注意边界情况。 |
| S3 | **NOPASSWD:ALL sudo 权限** | 高 [归档] | 第34行给 developer 用户无密码完整 sudo 权限。配合 Claude Code 全权限配置,developer 用户等同于 root。这是有意的设计决策(全权限沙箱),但安全边界完全消失。**同上,归档文件中的配置。** |
| S4 | **.bashrc 中 PATH 包含 `$PATH`** | 正面 | 第66行 `export PATH="/usr/local/bin:/usr/bin:/bin:\$PATH"` 使用 `\$PATH` 在双引号 heredoc 中转义 `$`,确保写入文件的是字面量 `$PATH` 而非展开后的值。这个转义是正确的。 |
#### 代码质量
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| Q1 | **功能模块划分清晰** | 正面 | 按顺序分为:开发工具检测 -> 用户创建 -> Claude 配置(root) -> Claude 配置(developer) -> 别名配置(root) -> 启动服务 -> 信息展示。结构清晰。 |
| Q2 | **root 和 developer 配置对称处理** | 正面 | settings.json 和 .bashrc 分别为两个用户配置,且 chown 所有权正确。 |
| Q3 | **alias 区分 root/developer 参数** | 正面 | root 用 `--allow-dangerously-skip-permissions`(第76行),developer 用 `--dangerously-skip-permissions`(第65行)。准确反映了 Claude Code 对 root 用户的限制。 |
| Q4 | **与 entrypoint.sh (Alpine) 大量重复** | 高 [归档] | 开发工具检测(第19-29行 vs 第19-29行完全相同)、dev-tools.sh 写入(第80-88行 vs 第32-40行完全相同)、cleanup 函数(第7-14行相同)、服务启动和检查(第90-106行几乎相同)。唯一差异是用户创建和 Claude 配置部分(第31-78行)。重复率约 65%。**因归档而不再是活跃问题。** |
| Q5 | **环境变量默认值处理良好** | 正面 | 第69-70行 `${API_TIMEOUT_MS:-3000000}``${CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:-1}` 提供了合理的默认值。 |
| Q6 | **端口/标题/密码硬编码** | 低 | 7682、2223、"WorkPod Test"、`jc:1234567` 全部硬编码。(注:此为已归档的 entrypoint-test.sh 中的历史值;当前基础实例已使用 2222/7681 端口,凭据通过环境变量注入) |
#### 代码质量评分:**7 / 10** (历史评分,文件已归档)
> 扣分项:与 entrypoint.sh 高度重复(-1.5)、SSHD_PID 未赋值(-1)、全权限安全策略未文档化(-0.5)
---
### ttyd-session.shtmux 会话管理)
**文件路径:** `E:/wk-lab/workpod/ttyd-session.sh`
#### 健壮性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| R1 | **set -e 缺失** | 中 | 脚本没有 `set -e`。如果中间某条命令失败(如 `tmux list-sessions`),脚本会继续执行可能导致错误状态扩散。考虑到脚本的交互性质(需要 read 输入),`set -euo pipefail` 更合适但需注意 `read` 在 set -e 下的行为(read 到 EOF 返回非零不会终止脚本,因为它是条件上下文的一部分)。 |
| R2 | **SESSION_NAME 初始为空字符串** | 低 | 第4行 `SESSION_NAME=""` 初始化为空。如果 TTYD_QUERY_STRING 解析也得到空值(第9行的 grep 无匹配),则进入交互菜单分支。流程正确但变量生命周期不够明确。 |
| R3 | **tmux session 名过滤后可能为空** | 中 | 第55行 `tr -cd 'a-zA-Z0-9_\-'` 过滤后如果 SESSION_NAME 变成空字符串(例如原始输入全是特殊字符),第58行 `tmux new-session -t ""` 会创建一个名为空字符串的 session。虽然 tmux 允许这样做但不推荐。应在过滤后检查是否为空并赋予默认值 "default"。 |
| R4 | **sed -n "${CHOICE}p" 未做范围校验** | 中 | 第45行 `sed -n "${CHOICE}p"` 直接将用户输入的数字传给 sed。虽然前面有 `grep -qE '^[0-9]+$'` 校验了纯数字,但没有检查数字是否在有效范围内(1 到 session 数量之间)。超出范围的 CHOICE 会导致 sed 输出空行,SESSION_NAME 被设为空字符串,然后触发 R3 的问题。 |
| R5 | **EXISTING 变量的 word splitting** | 低 | 第20行 `tmux list-sessions` 输出类似 `my_session: (attached)` 格式,awk 提取第一列后得到 `my_session:`(带冒号)。第24行 `for s in $EXISTING` 依赖 word splitting 来遍历 session 名。如果 session 名包含空格(虽然 tmux 不允许),会出错。实际风险低。 |
| R6 | **exec tmux attach/new 替换进程** | 正面 | 第59-61行使用 `exec` 替换当前 shell 进程为 tmux,避免多余的 shell 进程残留。这是正确的做法。 |
#### 可移植性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| P1 | **BusyBox awk 兼容性(已修复)** | 已解决 | 历史上使用 `grep -oP`Perl 正则)解析 query string,在 BusyBox 中不可用。当前版本第9行改用 `tr '\&' '\n' \| grep '^session=' \| cut -d= -f2` 的管道链,完全兼容 BusyBox。 |
| P2 | **tr -cd 字符类兼容性** | 低 | 第55行 `tr -cd 'a-zA-Z0-9_\-'` 中的 `\-` 在 GNU tr 和 BusyBox tr 中都能正确解释为字面量连字符。POSIX 标准中字符类内的 `-` 放在首位或末位或转义均可。 |
| P3 | **printf 格式化** | 正面 | 第25行 `printf "║ [%d] %-33s║\n" "$I" "$s"` 使用标准 printf 格式,跨实现兼容。 |
| P4 | **clear 命令** | 信息 | 第51行 `clear` 依赖 terminfo 数据库。在 Docker 的 ttyd 环境中通常可用。 |
| P5 | **grep -qE** | 正面 | 第44行使用 `grep -qE`(扩展正则),BusyBox awk 支持此选项。 |
#### 安全性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| S1 | **TTYD_QUERY_STRING 注入防护** | 正面 | 第9行通过管道链 `tr \| grep \| cut` 解析,最终结果经过第55行 `tr -cd` 过滤为安全字符集。即使攻击者构造恶意的 query string(如 `session=;rm -rf /`),也会被过滤掉。安全性良好。 |
| S2 | **用户输入 SESSION_NAME 注入防护** | 正面 | 第55行将用户交互输入同样通过 `tr -cd` 过滤,tmux session 名被限制在 `[a-zA-Z0-9_-]` 字符集中。无法注入 shell 命令。 |
| S3 | **无外部命令拼接** | 正面 | 所有变量传递给 tmux 时都加了双引号(`"$SESSION_NAME"`),防止 word splitting 和 globbing。 |
#### 代码质量
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| Q1 | **UI 设计精美** | 正面 | 使用 Unicode 制表符绘制边框,视觉效果专业。编号选择 + 自定义名称的双重输入方式用户体验好。 |
| Q2 | **双模式设计合理** | 正面 | URL 带 `?session=xxx` 直接进入(自动化场景),不带参数显示菜单(手动场景)。覆盖了两种主要使用方式。 |
| Q3 | **默认值 fallback** | 正面 | 第39-41行用户输入空时默认为 "default"。 |
| Q4 | **缺少 set -e/o pipefail** | 低 | 如 R1 所述。对于这种交互式脚本,建议至少加 `set -uo pipefail`(不含 -e 以免影响 read)。 |
| Q5 | **冒号未从 session 名中去除** | 低 | 第20行 awk `{print $1}` 提取的是 `session_name:`(带尾部冒号)。这个带冒号的名字会被用于显示(第25行)和选择(第45行 sed 取行),最终传入 tmux 的也是带冒号的名字。tmux 本身允许 session 名包含冒号,所以功能不受影响,但显示上不够干净。可以用 `tr -d ':'` 清理。 |
| Q6 | **代码简洁高效** | 正面 | 仅 63 行实现了 query string 解析、交互式菜单、session 创建/附加的完整功能。没有冗余逻辑。 |
#### 代码质量评分:**8 / 10**
> 扣分项:缺少 set -euo pipefail-0.5)、session 名空值/超范围未防护(-1)、冒号未清理(-0.5)
---
## 跨脚本问题
| # | 问题 | 影响范围 | 说明 |
|---|----------|----------|------|
| X1 | **entrypoint.sh 与 entrypoint-test.sh(归档) 高度重复** | 2 个文件(其中1个已归档) | 开发工具检测(12行)、dev-tools.sh 写入(8行)、cleanup函数(8行)、trap注册(1行)、SSH启动+检查(10行)、ttyd启动+检查(8行)、信息展示(18行)、exec sleep(1行) —— 约 66 行完全相同,占总代码量 66/210 = 31%(不计公共部分则重复率更高)。**优先级大幅降低(entrypoint-test.sh 已归档)。** |
| X2 | **三个版本的 cleanup 函数逐字相同** | 3 个文件 | Ubuntu/Alpine/Test 三份 entrypoint 的 cleanup 函数(第7-14行)完全一致。这是典型的 DRY 违反。 |
| X3 | **ttyd 认证密码三套不同策略** | 3 个文件 | Ubuntu版无密码 / Alpine版 `ada:123` / Test版 `jc:1234567`。安全级别不一致,容易在部署时混淆。**[已修复]** — 当前版本统一从 `${TTYD_CREDENTIALS}` 环境变量读取。 |
| X4 | **dev-tools.sh 内容在 Alpine 和 Test 版中逐字相同** | 2 个文件 | entrypoint.sh 第80-88行与 entrypoint-test.sh 第32-40行的 heredoc 内容完全一致。 |
| X5 | **SSHD_PID 跟踪在所有版本中都无效** | 3 个文件(其中1个已归档) | 三个版本的 entrypoint 都声明了 `SSHD_PID=0` 且从未赋值,cleanup 中的 `kill "$SSHD_PID"` 分支永远不会执行。这是一个系统性缺陷。(低优先级,未修复) |
| X6 | **端口号/标题/密码散落在各脚本中** | 3 个文件(其中1个已归档) | 修改端口或密码需要在多个文件中同步修改,遗漏任一文件会导致配置不一致。**[已改善]** — 当前版本已集中到 docker-compose.yml 的 environment 配置中管理。 |
---
## Top 10 改进建议
| 优先级 | 建议 | 涉及脚本 | 收益 |
|--------|-------|----------|------|
| P0 | **重构 entrypoint 公共框架(配置驱动)** | entrypoint.sh (x2, 其中1个已归档) | 消除 ~66 行重复代码,新增容器变体只需配置文件(优先级降低:entrypoint-test.sh 已归档) |
| P1 | **修复 SSHD_PID 追踪(统一方案)** | entrypoint.sh (x3) | 实现 sshd 优雅关闭,防止孤儿进程 |
| P2 | **ttyd-session.sh 添加 set -uo pipefail + 输入校验** | ttyd-session.sh | 防止空 session 名和越界访问 |
| P3 | **统一 ttyd 认证策略,密码从环境变量读取** | entrypoint.sh (x3) | 消除安全配置不一致,支持灵活部署(**已完成**) |
| P4 | **ttyd-session.sh 清理 session 名尾部冒号** | ttyd-session.sh | UI 显示更干净,避免意外行为 |
| P5 | **entrypoint-test.sh sudoers 文件写入增加原子性** | entrypoint-test.sh | 防止中断导致 sudo 不可用 |
| P6 | **document 全权限安全策略的影响范围** | entrypoint-test.sh | 让使用者明确了解 `allow:["*"]` + `NOPASSWD:ALL` 的安全含义 |
| P7 | **端口/标题/密码等配置外部化为环境变量** | entrypoint.sh (x3) | docker-compose.yml 集中管理,消除散落配置 |
| P8 | **考虑 entrypoint.sh shebang 改为 /bin/sh** | entrypoint.sh (Alpine x2) | 减少 bash 依赖,Alpine 环境更轻量(需确认无 bashism) |
| P9 | **sshd 启动增加 -D 模式或 PID 文件追踪** | entrypoint.sh (Alpine x2) | 更精确的进程生命周期管理 |
---
## 附录:历史 Bug 回顾
| Bug | 影响 | 当前状态 |
|-----|------|----------|
| grep -P 在 BusyBox 不可用 | Alpine 环境 TTYD_QUERY_STRING 解析失败 | ttyd-session.sh 第9行已改用 `tr & '\n' \| grep '^session=' \| cut -d= -f2` 管道链,完全兼容 BusyBox |
| heredoc 单引号阻止变量展开 | .bashrc 中 ANTHROPIC_AUTH_TOKEN 等环境变量无法传递到 developer 用户 | entrypoint-test.sh 第64行已改用 `<< BASHRC` 双引号 heredoc,变量在写入时正确展开;`\$PATH` 转义确保字面量输出 |
| dev-tools.sh 反斜杠转义问题 | profile.d 脚本中 `\$PATH``\\` 导致语法错误 | 当前版本 dev-tools.sh 使用单引号 heredoc `<< 'PROFILE'`,内容原样写入无需转义,已修复 |