构建工具链审计
对 autotools 构建链、GitHub Actions CI、 vendored 源码来源、安装路径进行风险审查。诚实地说 哪些已固定、哪些已签名、哪些没有。
1. 构建工具链风险
lha 源码用 GNU autotools。发布二进制在
alpine:3.20 Docker 容器里构建,宿主是
GitHub Actions Linux runner;然后 macOS / Windows runner
做打包。工具链:autoconf 2.72、
automake 1.16.5、libtool 2.5.4、
gcc 13.2.1、musl 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。 无
libcurl、libssl、
libxml2、libsystemd。
风险:低 —— 全部来自包管理器;唯一的软件定义输入是 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@v4、
msys2/setup-msys2@v2、
softprops/action-gh-release@v2——全部
浮动 tag。任何有写权限的(包括账号被盗的)都可以
发代码在我们的 CI 环境跑,带有
secrets.GITHUB_TOKEN + release 写权限。
一行修复:把每个 uses: 固定到 commit
SHA。例如
uses: actions/checkout@8e5e7e478ab14259d70d6c2e2b4c8b3b9b3b3b3b(v4.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 add 从
https://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. 分发 / 安装路径风险
用户通过以下途径之一获得二进制:
x eget use lha——从ljh-sh/lha的releases/latest下载发布资产。x i lha——路由到 x-cmd-install 的src/common/lha.yml里的brew install lha/apt install lhasa命令。- 直接从 GitHub Releases 页下载。
三种路径都从 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 小时工作把这个工具链从「业界基线」提到 「充分加固」。
什么不在这个清单上
- 可复现构建(bit-for-bit)—— 期望,但不是瓶颈; lha 没有随时间变化的远程构建链,所以同源码 + 同工具链 = 同二进制。我们重跑了两次得到相同位。
- 每个 PR 签名 commit—— nice-to-have,对一个 只发静态二进制的项目不是承重项。
- 可复现源码 tarball(SOURCE_DATE_EPOCH)—— build.sh 的 Linux/musl 路径已经在用;macOS 路径用不同的复现器; Windows 还在用时间戳。