对
flutter-ohos-builder(GitHub Action)+flutter-ohos-app-template(示例仓库)两个公开仓库做的一次完整体检:查密钥泄漏、查 CI 浪费、查逻辑缺陷,然后把发现的问题分三批改掉并逐个验证。
这两个仓库是我维护的 Flutter 鸿蒙(HarmonyOS)CI 构建方案:前者是打包 HarmonyOS .hap / .app、Android APK 和 Web 的 Docker Action,后者是配套的示例工程,带 9 个示范 workflow。
前一轮刚踩过一个坑:Android 签名的 secret 改了名(ANDROID_STORE_BASE64 → ANDROID_SIGN_STORE_BASE64),但 4 个 workflow 还引用旧名。GitHub 对不存在的 secret 不报错,只渲染成空字符串,于是构建静默降级成 debug 未签名包,一路绿灯 "success",直到下载产物用 apksigner 验证才发现。
这件事说明:这类仓库的问题不会以报错的形式出现,只会以"看起来一切正常"的形式出现。所以值得做一次系统性体检。
不是只看当前代码,而是四条线并行:
| 维度 | 做法 |
|---|---|
| 源码 | 拉取两个仓库全量代码,逐行审读 entrypoint.sh、Dockerfile、9 个 workflow |
| 历史 | 遍历两仓库共 63 个 commit 的全部 patch,正则匹配私钥块、storePassword=、各类服务 Token 前缀、base64 长串、.jks / .p12 / key.properties 文件名 |
| 平台侧 | 调 GitHub API 查 Secret Scanning alerts、Dependabot alerts、分支保护状态 |
| 产物侧 | 审已推送镜像每一层的 ENV / ARG / history;检查上传的 artifact 清单 |
第三条线尤其重要:Secret Scanning 只认已知厂商格式的密钥,自定义的 keystore base64 和明文密码它扫不出来,所以历史 commit 必须自己扫。
结论:历史干净。 63 个 commit 零命中,Secret Scanning 也是 0 alert。日志输出、artifact 清单、Dockerfile、已推送镜像层——全部没有泄漏。
但静态代码里发现了一个正在发生的泄漏敞口。
entrypoint.sh 会给 OHOS 构建注入 hvigor 的 signingConfigs 块,其中 keyPassword / storePassword 是明文,写入位置是 ohos/build-profile.json5——注意,这是挂载进容器的工作区文件,不是 /tmp。
对比 Android 侧的待遇,差距很明显:
| Android | OHOS | |
|---|---|---|
| 密码落地位置 | /tmp/android-sign/(容器外) |
ohos/build-profile.json5(工作区) |
| 构建后清理 | cleanup() 删除 + 删 /tmp/android-sign |
只删 /tmp/android-sign,不碰这个文件 |
.gitignore 兜底 |
有 | 无 |
CI 上风险不高(runner 用完即毁)。真正的危险在本地跑:
docker run --rm -v "$(pwd):/workspace" builder --build-target app ...
# 之后
git add -A && git commit # ← 明文密码进了公开仓库
ohos/ 是 flutter create 的生成物,untracked 且不在 .gitignore 里,一次 git add -A 就带走。这是那种"不会立刻出事、但迟早出事"的洞。
builder 的英文 README 有 6 处还写着 ANDROID_STORE_BASE64 / ANDROID_STORE_PASSWORD——这两个 secret 已经被删掉了。照抄文档的人得到空串,静默打出 debug 包。这正是我们上一轮踩的那个坑,只是这次在文档里,会坑到所有使用者。
.dockerignore部署镜像的 Dockerfile 用了 COPY . .,没有 .dockerignore 意味着 .git/(完整提交历史)和任何躺在目录里的签名文件都会进 builder stage。
今天不泄漏——最终镜像只 COPY build/web。但只要有人为了让构建快一点加上 cache-to: type=registry(这是最常见的加速手段),stage1 的层就会被推到 registry,公开可拉。这是个定时炸弹。
| 位置 | 引用 |
|---|---|
| 00/01/02/03 | uses: zhongdaiqi/flutter-ohos-builder@main |
| 07/08/09 | ghcr.io/zhongdaiqi/flutter-ohos-builder:latest |
| 03/04 | zhongdaiqi/flutter-ohos-builder:web-latest |
@main 和 :latest 意味着 builder 仓库的下一次推送会立刻改变所有使用者执行的内容——而这个容器在命令行参数里被直接递交签名密码。
selftest.yml 里有一处 set -x。当前没有 secret 参与所以安全,但 set -x + secret 是致命组合,且该 job 会把日志强推到公开分支。permissions:,继承仓库默认(通常是全部读写)。配额杀手:selftest.yml 挂在 on: push + pull_request 上,每次提交都现场构建那个 8GB 镜像(HarmonyOS CLT 6.8GB + Android SDK + Flutter),30–60 分钟。README 改个标点也会触发。公开仓库的 fork PR 同样能触发。
一段死代码:OHOS 构建分支在 set -euo pipefail 下执行 flutter build,非零退出码会直接终止脚本。而它下面 8 行的 FLUTTER_RC 判断——包括注释里承诺的"debug 模式 hvigor 返回非 0 但产物已生成,视为成功"——永远不会执行。Android 分支写法是对的(用了 set +e + 捕获退出码),OHOS 分支没对齐。
纯浪费:07/08/09 是"Docker 加速"示范,只做 docker pull + docker run,从不构建镜像,却带着一个 Free disk space 步骤,其中的 docker image prune -af 白烧 60 秒还会清掉 runner 的镜像缓存。
名不副实:release.yml 的步骤还叫 "Build and push multi-arch image",但早已改成单 amd64(修 arm64 失败时改的);里面的 Set up QEMU 对单平台构建毫无作用。
给 entrypoint.sh 加 redact_build_profile(),挂进 cleanup() 的 trap EXIT(覆盖成功 / 失败 / set -e 中断全部路径),把 build-profile.json5 里的两个密码替换成 ******;同时补上 rm -rf /tmp/ohos-sign。.gitignore 追加 ohos/build-profile.json5 作为第二道防线。
两个细节值得说:
cleanup() 之前。 脚本在参数解析处就可能 exit 1,如果那时函数还没定义,cleanup 会中断、tee 不被回收——这个坑历史上真的挂过 85 分钟(tee 子进程继承了 stdout,hvigor 守护进程一直不松手)。"keyPassword" 时才改写,未签名的构建完全不受影响。顺手修了 OHOS 分支的 set +e(对齐 Android)和重复执行的 flutter doctor -v。同时把 selftest 加了 paths filter 和 concurrency。
Free disk space。00/01/02/03 保留——它们用 uses:,是真的在现场构建 8GB 镜像,腾空间有必要。判断标准不是"这个步骤看起来占时间",而是"这台 runner 到底要不要构建镜像"。release.yml 删掉 QEMU 步骤,步骤改名。.dockerignore。@main → @v1.4,ghcr :latest → :v1.4。web builder 保持 :web-latest 并加注释说明——它不碰签名代码路径,pin 了反而每次发版都要手动跟进。之所以要先打 tag:pin 一个还没修的版本等于把漏洞钉死。v1.4 触发 Release 后,确认 ghcr 上 v1.4 和 latest 已经是同一个 digest,也就是说连浮动引用拉到的都是修复版。
11 个 workflow 补顶层 permissions: contents: read;web-build.yml 补 GHA 缓存(注释里写明不要改成 type=registry);注释里说明容器以 root 运行导致产物属主是 root。
仓库设置上,两仓库都开了 Dependabot security updates + Secret Scanning push protection;main 加了分支保护,但只设了禁强推 + 禁删除,没有开"要求 PR"和"要求状态检查"——那会打断直接 push main 的习惯,收益抵不上摩擦。
没有一项是"改完就当好了"。
| 验证项 | 结果 |
|---|---|
bash -n + 18 个 YAML 全量校验 |
通过 |
| 脱敏正则单测(签名 / 未签名 / 文件不存在三种输入) | 通过;keyAlias、compileSdkVersion 等非密字段不受影响,脱敏后仍是合法 JSON |
01. OHOS 打包 真实构建 |
success,产物 ohos-default-signed.app |
| 该 run 的构建日志 | [cleanup] redacted keyPassword/storePassword ×2;日志里密码只以长度出现(80 characters),profile 打印为 ****** |
builder Release(v1.4,删 QEMU 后首次) |
success;ghcr v1.4 与 latest digest 一致 |
07 用 :v1.4 且删掉清理步骤后 |
success,5m07s(改前约 6.1min,省 ~60s),日志同样有 redacted |
00 在收窄 permissions 后 |
success |
最后一项顺带验证了一件我原本没把握的事:upload-artifact@v4 在只有 contents: read 的情况下依然能正常上传,不需要额外开 actions: write。
\ 续行的 shell 命令加注释,注释不能插在续行中间,那会把命令截断。必须放在 run: | 块的开头。第一次写 04 的时候就写错了。USER 指令时容器默认以 root 跑,写进挂载目录的文件宿主是 root。CI 上无所谓,本地会咬人。审计清单基本清空。剩两项属于使用习惯取舍,保留原状:
delete-all-actions.yml(能一键删光所有 workflow runs 和 artifact)ci-diagnostics 分支这次最有价值的发现不是某个具体的漏洞,而是一个规律:这套 CI 里 Android 那条路被反复打磨过(两层防护、fail-fast 校验、404 行注释解释每个陷阱),OHOS 那条路是后来照着写的,但只抄了功能,没抄防护。
照抄代码时最容易丢的,恰恰是那些"看不见的地方"——清理逻辑、gitignore 兜底、注释里写的失败模式。所以跨分支、跨平台复用代码时,值得专门问一句:安全相关的部分,抄全了吗?
还没有评论,登录后来说两句。