Flutter OHOS 构建器 CI 安全审计与优化全记录

xiezitai · 2026-10-12 05:42 · 30 次阅读
FlutterOHOSCI

对 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、已推送镜像层——全部没有泄漏。

但静态代码里发现了一个正在发生的泄漏敞口。

三、安全发现

P0:OHOS 签名密码明文落在工作区,零防护

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 就带走。这是那种"不会立刻出事、但迟早出事"的洞。

P1:文档里的 secret 名早已失效

builder 的英文 README 有 6 处还写着 ANDROID_STORE_BASE64 / ANDROID_STORE_PASSWORD——这两个 secret 已经被删掉了。照抄文档的人得到空串,静默打出 debug 包。这正是我们上一轮踩的那个坑,只是这次在文档里,会坑到所有使用者。

P1:缺 .dockerignore

部署镜像的 Dockerfile 用了 COPY . .,没有 .dockerignore 意味着 .git/(完整提交历史)和任何躺在目录里的签名文件都会进 builder stage。

今天不泄漏——最终镜像只 COPY build/web。但只要有人为了让构建快一点加上 cache-to: type=registry(这是最常见的加速手段),stage1 的层就会被推到 registry,公开可拉。这是个定时炸弹。

P1:供应链,全是浮动引用

位置 引用
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 挂在 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 作为第二道防线。

两个细节值得说:

  1. 函数定义必须放在 cleanup() 之前。 脚本在参数解析处就可能 exit 1,如果那时函数还没定义,cleanup 会中断、tee 不被回收——这个坑历史上真的挂过 85 分钟(tee 子进程继承了 stdout,hvigor 守护进程一直不松手)。
  2. 只在文件里真的存在 "keyPassword" 时才改写,未签名的构建完全不受影响。

顺手修了 OHOS 分支的 set +e(对齐 Android)和重复执行的 flutter doctor -v。同时把 selftest 加了 paths filter 和 concurrency。

第二批:省时间与加固

之所以要先打 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。

七、踩坑记录

  1. 给 \ 续行的 shell 命令加注释,注释不能插在续行中间,那会把命令截断。必须放在 run: | 块的开头。第一次写 04 的时候就写错了。
  2. secret 改名是静默漂移。GitHub 对不存在的 secret 渲染成空串、不报错、不失败。所以改名后必须 grep 全部 workflow 的历史版本,只看当前版会漏。今天清理的两个悬空 secret(一个零引用、一个真重复)就是这么查出来的。
  3. 镜像没有 USER 指令时容器默认以 root 跑,写进挂载目录的文件宿主是 root。CI 上无所谓,本地会咬人。
  4. 浮动标签引用的风险容易被低估。自己管两个仓库时很容易觉得"都是我的代码,没事",但它同时服务于 GitHub Marketplace 上的外部使用者。

八、还剩什么

审计清单基本清空。剩两项属于使用习惯取舍,保留原状:

小结

这次最有价值的发现不是某个具体的漏洞,而是一个规律:这套 CI 里 Android 那条路被反复打磨过(两层防护、fail-fast 校验、404 行注释解释每个陷阱),OHOS 那条路是后来照着写的,但只抄了功能,没抄防护。

照抄代码时最容易丢的,恰恰是那些"看不见的地方"——清理逻辑、gitignore 兜底、注释里写的失败模式。所以跨分支、跨平台复用代码时,值得专门问一句:安全相关的部分,抄全了吗?

评论

还没有评论,登录后来说两句。