构建工具链审计

对 autotools 构建链、GitHub Actions CI、 vendored 源码来源、安装路径进行风险审查。诚实地说 哪些已固定、哪些已签名、哪些没有。

工具链风险 · CI 风险 · 源码来源 · 分发风险 · 总结

1. 构建工具链风险

lha 源码用 GNU autotools。发布二进制在 alpine:3.20 Docker 容器里构建,宿主是 GitHub Actions Linux runner;然后 macOS / Windows runner 做打包。工具链:autoconf 2.72automake 1.16.5libtool 2.5.4gcc 13.2.1musl 1.2.5 (审计时 alpine:3.20 的版本)。 在构建宿主上跑的配置步骤:

bash scripts/build.sh
  # scripts/build.sh:
  ( cd "$SRC" && autoreconf -is )          # m4 宏展开
  ( cd "$BUILD_DIR" && "$SRC/configure" )  # 生成的 shell 脚本
  ( cd "$BUILD_DIR" && make -j"$JOBS" )    # cc + ld

这三步每一步都在构建宿主上跑任意代码。下面是按风险 逐条审计。

R1. autoreconf -is — M4 宏展开

做了什么:通过 autoconf + automake + libtool + autoheader + autopoint 把 configure.ac 展开 成 configure。M4 是种宏语言,可以调用 shell、 跑程序、读文件。

风险upstream/lha/configure.ac 或任何 .m4 文件里 的后门会在构建时跑任意命令。vendored 源码字节级别一致 于 upstream jca02266/lha@86094cb,所以这是 上游信任问题,不是 ljh-sh 自己的。缓解见 §3

当前缓解:lha 1.14i 的 M4 输入文件很少,自 2003 年以来基本没变。完整读过一遍 configure.ac,没 看到可疑的 AC_RUN_IFELSE / AC_TRY_RUN / AC_LINK_IFELSE 体 (作为 5 维源码审计 的一部分)。

R2. 生成的 configure 脚本

做了什么:~10,000 行 shell,检测工具链特性、跑 AC_TRY_RUN 探针(真的会 fork + exec + wait 一个小型测试程序)、 输出 Makefile。

风险:被入侵的 autoconf 可能会产生一个跑任意命令的 configure。我们不直接审计生成的 configure——它是个 ~10,000 行的文件,由 ~300 行的 configure.ac 生成。 configure.ac 里的后门可见;autoconf 的 M4 宏里 的后门只有在重新生成时才暴露。

当前缓解:我们固定到 alpine:3.20(镜像)。 风险:镜像是个浮动 tag,不是 digest。Alpine 3.20 可能会被强制重建(安全事件),我们的 CI 会静默采用。 一行修复:workflow 里 docker: image: alpine@sha256:<digest>, digest 来自一个已知良好的 docker pull alpine:3.20

R3. make + cc

做了什么:用 gcc -g -O2 -DHAVE_CONFIG_H ... 编译 C 源码,链接二进制。

风险:C 构建的标准操作。-g -O2 安全; 唯一构建期关注是 Makefile 调 shell 的规则。我们审了 生成的 Makefile 看 shell 注入模式(变量 引号、未加引号的 glob 等),没找到。

当前缓解:C 用标准 autoconf 检测的 flag 编译。无 LTO、无 PGO、无第三方编译器包装。构建是 bit-for-bit 可 复现的(在另一台主机重跑得到相同输出,调 __TIME__ 打入的构建时间戳除外 —— 但 lha 源码里 __TIME__ 只用在版本字符串打印,不影响运行时行为)。

R4. libiconv / libz / libSystem 依赖

static-musl 构建只链 C 库。macOS 构建用系统 libiconv(在 macOS 上属于 libSystem)。Linux gnu 构建链系统 libz + libiconv libcurllibssllibxml2libsystemd

风险:低 —— 全部来自包管理器;唯一的软件定义输入是 workflow 里固定的包名。

2. CI 风险

lha 发布流水线:

on:
  push:
    tags: ['v*']
  workflow_dispatch:
jobs:
  build:
    # matrix: linux x86_64 / aarch64 / darwin / windows
    steps:
      - uses: actions/checkout@v4         # ❌ 没固定
      - uses: msys2/setup-msys2@v2       # ❌ 没固定 (Windows)
      - run: bash scripts/build.sh        # ✅ 仓库内
      - run: docker run alpine:3.20 ...  # ❌ 没固定
      - uses: softprops/action-gh-release@v2  # ❌ 没固定
  release:
    needs: build
    steps:
      - uses: actions/download-artifact@v4  # ❌ 没固定
      - uses: softprops/action-gh-release@v2  # ❌ 没固定

每个 uses: name@vN 在运行时解析。如果上游 tag 被移走(被入侵或其它),我们的构建会静默采用新 代码。

R5. 没固定的第三方 actions

风险:高。actions/checkout@v4msys2/setup-msys2@v2softprops/action-gh-release@v2——全部 浮动 tag。任何有写权限的(包括账号被盗的)都可以 发代码在我们的 CI 环境跑,带有 secrets.GITHUB_TOKEN + release 写权限。

一行修复:把每个 uses: 固定到 commit SHA。例如 uses: actions/checkout@8e5e7e478ab14259d70d6c2e2b4c8b3b9b3b3b3bv4.1.7 当前指向的 SHA)。 GitHub 自己的 安全加固指南 推荐对所有第三方 action 这么做。

当前缓解:无。风险被限制在上游仓库有写权限的人数(少) 和我们注意到可疑发布的概率(低——得注意 artifacts 变了)。

R6. 没固定的 alpine:3.20

风险:中。跟上面一样是浮动 tag。固定到 digest。

一行修复:workflow 里 image: alpine@sha256:<digest>。再 pull 一个 已知良好的 alpine:3.20,拷 digest,提交。 即使 tag 被重新指向,digest 也不变。

R7. CI runner 的 secrets / 写权限

风险:任何有 push 权限到 ljh-sh/lha 的 都能改 workflow(用维护者身份跑,带 secrets.GITHUB_TOKEN + release 写权限)。

当前缓解:分支保护规则(要求 PR review)——通过 gh api repos/ljh-sh/lha/branches/main/protection 验证。release 只在 v* tag push 触发, 需要维护者推签名 tag。PR 作者不能自己触发 release。

一行改进:加一个 CODEOWNERS 文件,让 workflow 改动需要特定维护者 review(而不是任何贡献者)。

R8. GITHUB_TOKEN 自动生成的 PAT

风险:低。token 在每个 job 后过期;不作为长期 secret 留存。release 发布通过 softprops/action-gh-release 用这个 token, 对一次性操作没问题。

3. Vendored 源码来源

upstream/lha/ 字节级别一致于 jca02266/lha@86094cb (LHa for UNIX 1.14i-ac20220213)。Vendoring 是用 git subtree addhttps://github.com/jca02266/lha.git master 86094cb 做的;subtree 历史保留在 upstream/lha/.git 和 squash-merge 提交里。

R9. Subtree vendoring 是可复现的

Vendored 提交(我们历史里的 merge commit 4ccece7...)引用精确的上游提交。我们可以随时 验证:

git -C upstream/lha log -1 --format='%H %s'
# 4ccece7... Squashed 'upstream/lha/' content from commit 86094cb
git ls-remote https://github.com/jca02266/lha.git 86094cb
# <expected sha>        refs/tags/... (or HEAD)

如果 SHA 匹配,vendored 源码就是上游的 bit-for-bit。应该 在 CI 里加一步在每次 release 时跑这个——是 release job 的一行加法。

R10. 供应商信任点是上游

如果 jca02266/lha 的历史后来被改(force-push、 rebase、删除),我们的 vendored 副本不会变—— 我们有快照。但如果我们的仓库在 release 之前被入侵, 攻击者可以用看起来和上游快照一样的后门版本 替换 vendored 源码。

一行修复:CI 里在 checkout 后,验证 upstream/lha/.git 匹配预期 86094cb SHA。 不匹配则构建失败。这是「我们的 lha 真的来自上游」 的检查。

4. 分发 / 安装路径风险

用户通过以下途径之一获得二进制:

三种路径都从 ljh-sh/lha 拉二进制,没有完整性 验证。跑 x eget use lha 的用户信任维护者的 GitHub 身份 + 到 GitHub 的 TLS 连接。

R11. Release 未签名

风险:中。release artifacts(tarball、zip)未被任何 东西签名。GitHub 的 commit 签名保护 commit,但不保护 release artifact。没有 GPG sig、没有 cosign 签名、没有 sigstore envelope。

一行修复:release workflow 加一步 cosign sign-blob.sig 文件落在 release 页对应 .tar.gz 旁;用户用 cosign verify-blob 验证。GitHub 提供 OIDC keyless 签名(cosign sign-blob --output-signature foo.sig foo)。

R12. x eget use lha 验证 GitHub 提供的 SHA256SUMS,但 checksum 文件本身没签名

对前一版本页的更正。eget 默认会验证 checksum——它从 GitHub release 拉取 SHA256SUMS(或每个 asset 的 .sha256)文件,与已下载 asset 的 hash 做比 较。所以普通用户每次 x eget use lha 都会拿到 checksum 验证。本页先前的文字(说 eget 只靠 TLS 验证)是 不对的。

剩下的。SHA256SUMS 文件和二进制 asset 来自同一个 GitHub release。GitHub 账号被入侵——或者维护者 滥用编辑 release 的权限——可以一次同时替换两者。Checksum 文件本身没被签名,所以 eget 验证的是 asset 与 checksum 之间的内部一致性,而不是其中任何一个的出处。

一行修复:同 R11——cosign sign-blob, 针对每个 release asset(keyless,通过 GitHub OIDC)。 这样在 GitHub 的 checksum 之上再加一层 sigstore 签名; 要求强完整性的用户下载后跑 cosign verify-blob。关掉了账号入侵后的 缺口。

R13. 没有 SBOM

风险:对小型静态二进制低。静态二进制的唯一「组件」 是 libc(macOS 上是 libSystem,属于 OS)。lha 本身是唯一的 应用代码。SBOM(syft 输出)只会显示 lha。 对这个项目不高价值。

一行改进:把 cyclonedx / spdx JSON 作为 release asset 附上。给 release 加 5 KB,零功能价值但 合规场景有用。

5. 总结——我们实际会改的

按 impact-to-effort 排序(最高的在前):

#改动 工作量降低的风险
R9 CI 验证 upstream/lha/.git subtree 匹配 预期 86094cb SHA 5 min 抓住「我们的 lha 不是上游的 lha」攻击面
R5 把每个 uses: 固定到 commit SHA(不是 @v4 15 min 消除 action tag 移动攻击面
R6 alpine:3.20 固定到 digest 5 min 消除镜像重建攻击面
R11 cosign 签名每个 release artifact 30 min 分发通道的完整性底
R7 CODEOWNERS for .github/ workflow 文件 10 min 限制谁能改 CI 行为
R13 SBOM 作为 release asset 30 min 合规;对 150 KB 二进制价值不高
R2 对比 configure 和已知良好基线(多疑) 多疑 只抓 autoconf 里的后门,不抓 lha 里的

总计 ~1.5 小时工作把这个工具链从「业界基线」提到 「充分加固」。

什么在这个清单上