CI/CD 跑着跑着就被偷了——我把 GitHub Actions 供应链攻击的三个入口全拆开了
我配了三年前端 CI/CD,今天才发现这个「方便」是团队最大的安全缺口——GitHub Actions 的供应链攻击,三个入口我全踩了一遍。
攻击者搞 CI/CD 不用攻你的服务器,跑着你 runner 的人、偷着你 secrets 的工作流,权限比你自己的开发机还高。
第一个入口:PR 标题直接跑脚本
GitHub Actions 给你暴露了一堆上下文:github.event.issue.title、github.event.pull_request.title、github.event.comment.body。很多 workflow 习惯直接拿来用:
- name: Build
run: npm run build -- --version=${{ github.event.pull_request.title }}
表面看是「用 PR 标题当版本号」,实际上攻击者只需要提一个 PR,标题写成:
v1.0.0" && npm owner add attacker && echo "hacked
GitHub 会把整个标题展开成 shell 命令的一部分。PR 一开,攻击者的代码就在有 secrets.GITHUB_TOKEN 和所有 Secrets 的环境里跑完了。
正确做法:用 toJSON() 把上下文序列化成安全字符串,或者用专门的 Action 做输入校验:
- name: Build
run: |
VERSION=$(echo '${{ toJSON(github.event.pull_request.title) }}' | jq -r '.')
npm run build -- --version="$VERSION"
第二个入口:npm install 哪个版本你说了算吗
很多团队的 package.json 写的是 "lodash": "^4.17.21",npm install 的时候如果 registry 配置是这样的:
npm install lodash --registry=https://registry.npmjs.org
这没问题。但如果 workflow 里是这样写的:
- run: npm install
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
而 NPM_TOKEN 是公司私有 npm 的 token。问题是 npm install 在找不到私有包的时候会自动回退到公共 npm,并且优先安装版本号更高的那个。
攻击者只要把你的私有包名 company-ui 找到,然后在 npm 上传一个 company-ui@999.0.0——npm 默认会安装最高版本,workflow 就把攻击者的恶意包跑起来了。
真正有效的防御是在 package.json 里明确指定私有 registry,或者用 .npmrc 锁定:
@company:registry=https://npm.company.com
registry=https://npm.company.com
第三个入口:打错一个字母,包就没了
npm install react 你打过,npm install reacct 呢?
typosquatting 就是注册和流行包名只差一个字母的假包:reacct、nodejs、lodahs。GitHub Actions 的 ubuntu-latest 镜像每次全新创建,攻击者专门扫描新建的 runner IP 段,一旦发现有人在跑 npm install 立即注册对应名称的包。
2025 年就有安全研究团队实测:一个流行的 npm 包如果被 typosquatting,从发现到有人中招平均只需要 4 小时。
防御策略:
- 团队统一用
package-lock.json或pnpm-lock.yaml,锁定到具体版本和 hash - 在
.npmrc里加verify hash配置 - 用
npm audit配合 CI,每次 install 后自动扫已知恶意包名
你的 CI/CD 其实比你想的肥
GitHub Actions runner 的 token 权限有多高?默认情况下有:
- 对 repo 的读写权限(可写代码、可删分支)
- 能访问所有 Secrets(数据库密码、API Key、生产环境部署凭证)
- 能触发其他 workflow(横向移动)
一旦 CI/CD 被攻破,攻击者拿到的不是一个用户账号,是整条部署链路。
下一步可以做的:
workflow_write权限默认关掉,需要才开- 给 Secrets 设置 Environment protection rules(要求人工审批才能访问)
- 跑完 CI/CD 立刻 revoke 临时 token,不要让 token 长期有效
- 用
actions/checkout时指定persist-credentials: false,减少凭证泄露面
GitHub 的 runner 有 2FA、有审计日志,看起来比普通服务器安全——但它跑的是你的供应链,供应链被控,比服务器被黑更难发现。
你的团队配过这些防护了吗?还是也在用 PR 标题直接跑命令?
评论区
登录后可评论。