# 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.sh(Alpine 版) **文件路径:** `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.sh(Test 容器入口) **[归档]** > 此文件已移至 `_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'` 单引号 heredoc,JSON 内容不会被 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.sh(tmux 会话管理) **文件路径:** `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'`,内容原样写入无需转义,已修复 |