正文
GitHub Actions 不是“能跑就行”。一个 workflow 能完成测试、构建、发布,只说明自动化链路通了;它同时也可能拿着仓库写权限、npm token、云账号、Docker Hub 密钥、SSH key 和生产部署权限在跑。第三方 Action、脚本注入、过宽的 GITHUB_TOKEN、公开日志和 artifacts 组合在一起,CI 就会从发布工具变成供应链攻击入口。
2025-03-14,StepSecurity 披露 tj-actions/changed-files GitHub Action 被攻陷。GitHub Advisory Database 后续记录为 CVE-2025-30066:攻击者修改多个版本标签,让这些标签指向恶意提交;恶意代码从 Actions Runner 进程内存里提取 secrets,并把结果写到 workflow 日志里。这个 Action 当时被 23000 多个仓库使用,公开仓库的构建日志尤其危险。
这起事件最值得开发者记住的不是某一个 Action 名字,而是攻击路径:你以为自己只是在复用一个“列出改动文件”的小工具,但这个工具运行在有 secrets、有 GITHUB_TOKEN、有源码和构建产物的环境里。GitHub 官方安全文档也提醒,第三方 Action 一旦被攻陷,就可能访问同一个 job 里可用的 secrets,并使用 GITHUB_TOKEN 对仓库执行操作。
上线前先查这几件事:
| 检查项 | 要证明什么 |
|---|---|
| 第三方 Action | 来源可信、版本不可被随意移动、用途必要 |
GITHUB_TOKEN | 每个 workflow/job 只拿最小权限 |
| secrets | 只给真正需要的 job,不给普通测试和外部 PR |
| 触发器 | pull_request_target、workflow_run 不处理不可信代码 |
| 日志和 artifacts | 不输出 token、环境变量、构建缓存和私有文件 |
| 发布凭据 | 优先用 OIDC 短期凭据,不长期保存云密钥 |
先列出所有 workflow
先知道仓库里到底有哪些自动化入口。
Get-ChildItem .github\workflows -Filter *.yml
Get-ChildItem .github\workflows -Filter *.yaml再扫所有第三方 Action:
rg -n "uses:\\s+[^\\s]+@[^\\s]+" .github\workflows把结果分成三类:
| 类型 | 示例 | 风险判断 |
|---|---|---|
| GitHub 官方 | actions/checkout@v4、actions/setup-node@v4 | 仍要固定版本和限制权限 |
| 平台官方 | docker/login-action、aws-actions/configure-aws-credentials | 重点查权限和 secrets |
| 个人/小团队 Action | owner/action@v1 | 重点查维护者、源码、版本固定和替代方案 |
如果一个第三方 Action 只做很小的事,比如列文件、拼字符串、发评论、读版本号,先判断能不能用几行 shell 或官方 CLI 代替。CI 里的依赖越少,供应链暴露面越小。
第三方 Action 先固定到 SHA
GitHub 文档说明,固定到完整 commit SHA 才是不可变引用。只写 @v1、@v4、@main、@master 都可能在上游标签或分支移动后执行不同代码。
先找可移动引用:
rg -n "uses:\\s+[^\\s]+@(main|master|v[0-9]+|latest)" .github\workflows风险较高的写法:
steps:
- uses: tj-actions/changed-files@v44
- uses: some-owner/some-action@main更稳的写法:
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
- uses: some-owner/some-action@f1c2d3e4a5b60718293a4b5c6d7e8f9012345678固定 SHA 不是“永远不升级”。更实际的做法是:
- 先把发布、打包、部署、签名、上传制品这些高权限 job 固定到 SHA。
- 给
.github/workflows/**加 CODEOWNERS。 - 用 Dependabot 或人工 PR 更新 Action 版本。
- 升级 PR 里看上游 release、commit、维护者和源码改动。
通过标准:高权限 workflow 不使用 @main、@master、@latest 或只有大版本号的第三方 Action。
GITHUB_TOKEN 默认先降到只读
很多 workflow 没有写 permissions,开发者也就不知道 GITHUB_TOKEN 当前能做什么。GitHub 文档建议按最小权限配置 GITHUB_TOKEN,因为 Action 即使没有显式传入 token,也可能通过 github.token 上下文访问它。
先扫哪些 workflow 没写权限:
rg -n "^permissions:" .github\workflows普通测试 job 可以先用只读权限:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
- run: pnpm install --frozen-lockfile
- run: pnpm test需要写 issue、发评论、上传 package、发布 release 时,只给对应 job 加对应权限:
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
id-token: write
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
- run: pnpm build不要把整个 workflow 都设成写权限:
permissions: write-all通过标准:测试、lint、typecheck 默认 contents: read;发布 job 单独拿 contents: write、packages: write、id-token: write 等必要权限。
secrets 只给真正需要的 job
secrets 最常见的错误是“为了方便所有 job 都能用”。测试 job、lint job、PR 检查 job 通常不应该拿生产部署密钥。
先扫 workflow 里用到了哪些 secrets:
rg -n "secrets\\.[A-Z0-9_]+|env:\\s*\\$\\{\\{\\s*secrets\\." .github\workflows再按用途归类:
| secret | 应该出现在哪里 | 不应该出现在哪里 |
|---|---|---|
NPM_TOKEN | npm publish job | lint、test、PR 检查 |
DOCKERHUB_TOKEN | image push job | 单元测试 |
AWS_ACCESS_KEY_ID | 部署 job,最好迁移到 OIDC | 普通构建 job |
SSH_PRIVATE_KEY | 受控部署 job | fork PR、依赖安装、第三方 Action 前置步骤 |
SENTRY_AUTH_TOKEN | sourcemap 上传 job | 所有 job 的全局 env |
不要把 secrets 放在 workflow 顶层 env:
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}更稳的是只放到需要的 step:
jobs:
publish:
runs-on: ubuntu-latest
steps:
- run: pnpm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}如果是生产部署,使用 GitHub Environments 和 required reviewers,让 job 进入生产环境前需要审批。这样可以避免普通分支或误触发 workflow 直接拿到生产 secret。
外部 PR 不要拿特权上下文
pull_request_target 和 workflow_run 很容易被误用。GitHub 文档明确提醒,不要让这些特权触发器 checkout 或执行来自不可信 PR 的代码。它们可能有仓库写权限,也可能访问 secrets。
先扫危险触发器:
rg -n "pull_request_target|workflow_run" .github\workflows危险组合:
on:
pull_request_target:
jobs:
test:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: pnpm install
- run: pnpm test这个组合会在特权上下文里执行外部 PR 的代码。更稳的分工是:
| 场景 | 建议触发器 |
|---|---|
| 跑外部 PR 测试 | pull_request,不给 secrets |
| 给 PR 加标签或评论 | pull_request_target,不 checkout 外部代码 |
| 构建完成后发布 | workflow_run,只信任受控 workflow 的产物 |
| 部署生产 | workflow_dispatch 或受保护分支 push,加 environment 审批 |
如果你必须用 pull_request_target,先确认 workflow 不执行 PR 分支代码、不读取 PR 里上传的脚本、不把 PR 标题/正文直接拼进 shell。
不可信输入不要直接拼进 run
PR 标题、issue 内容、commit message、分支名、用户名都可以被攻击者控制。把这些值直接拼进 shell,可能变成脚本注入。
危险写法:
- name: Check title
run: |
echo "${{ github.event.pull_request.title }}"更稳的写法是先放到环境变量,再用 shell 变量处理:
- name: Check title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
printf '%s\n' "$PR_TITLE"如果这一步要调用 AI Agent、代码生成工具或自动修复工具,不可信输入还可能进入 prompt。此时还要限制工具权限,不要让它在同一个 job 里拿到发布 token、云凭据或仓库写权限。
日志和 artifacts 要按泄露处理
tj-actions/changed-files 事件里,secrets 的风险路径就是 workflow 日志。很多团队只盯着源代码和 secrets 配置,却忘了日志、artifacts、缓存和测试报告也会被下载或长期保留。
先扫明显会打印环境变量的命令:
rg -n "printenv|env|set|cat\\s+\\.env|echo\\s+\\$\\{|Write-Host\\s+\\$" .github\workflows再查上传 artifacts 的步骤:
rg -n "actions/upload-artifact|path:" .github\workflows高风险输出包括:
- 完整环境变量。
.env、.npmrc、.pypirc、kubeconfig、SSH key。- 测试失败时的完整 HTTP headers。
- 打包后的 sourcemap,包含源码和内部路径。
- Docker build logs 里的 build args。
- AI Agent 的执行日志、prompt、工具调用结果。
通过标准:workflow 日志里不能出现 secret 原文;artifacts 不包含 .env、私钥、token 配置、生产配置、构建缓存和上传文件原文。
发布凭据优先换成 OIDC
长期云密钥放在 GitHub Secrets 里,一旦 workflow、第三方 Action 或日志出问题,就要按泄露处理。GitHub OIDC 文档说明,workflow 可以向云厂商换取短期 token,减少长期云凭据复制到 GitHub 的需要。
发布 job 可以长这样:
permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
- name: Configure cloud credentials
uses: cloud-provider/login-action@0123456789abcdef0123456789abcdef01234567
- run: pnpm run deploy检查云侧 trust policy 时,至少限制:
| 条件 | 示例 |
|---|---|
| 仓库 | 只允许 repo:org/repo |
| 分支 | 只允许 refs/heads/main |
| 环境 | 只允许 environment:production |
| workflow | 只允许指定 workflow 文件 |
| 权限 | 只允许部署需要的云资源 |
通过标准:生产部署不依赖长期 AWS_SECRET_ACCESS_KEY、AZURE_CLIENT_SECRET、GCP_SERVICE_ACCOUNT_KEY 这类静态密钥;必须保留时,限制到单 job、短保留、可轮换、可审计。
workflow 文件要有人审
.github/workflows/** 的改动不只是“CI 配置”。它可能改变仓库权限、发布路径、secret 使用方式和部署目标。
加 CODEOWNERS:
.github/workflows/** @your-org/platform-team @your-org/security-team再配分支保护:
| 规则 | 目的 |
|---|---|
| workflow 改动必须 review | 防止悄悄加恶意 job |
| 禁止普通成员修改 Actions 设置 | 防止放开第三方 Action 策略 |
| 发布环境 required reviewers | 防止误触发生产部署 |
| 禁止 Actions 创建/批准 PR | 防止自动化绕过人工审查 |
通过标准:任何新增 third-party Action、secret、permissions: write、pull_request_target、部署 job,都必须有人看过。
可以用工具扫一遍 workflow
人工检查之外,可以跑 workflow 安全扫描。
OpenSSF Scorecard 可以检查仓库安全实践:
scorecard --repo=github.com/your-org/your-repozizmor 可以静态分析 GitHub Actions workflow:
zizmor .github\workflows工具发现的问题不能直接替代人工判断。它们适合抓这些常见风险:
- Action 没固定到 SHA。
GITHUB_TOKEN权限过宽。pull_request_target使用危险。- 不可信输入拼进 shell。
- secrets 暴露到过宽的 job。
- workflow 复用边界不清楚。
如果工具提示和业务需要冲突,先把例外写清楚:哪个 job、为什么需要、谁批准、失败后怎么轮换凭据。
已经用过可疑 Action 怎么办
如果发现项目用过被攻陷 Action,或者某次 workflow 日志里出现了 secret,不要只把 uses: 改掉。
按泄露处理:
- 先停用相关 workflow,避免继续执行。
- 搜索所有仓库里是否使用同一个 Action。
- 检查受影响时间段的 workflow logs。
- 下载或保全必要证据,但不要把 secret 复制到聊天、issue 或工单里。
- 轮换可能暴露的 secrets:GitHub token、npm token、云密钥、SSH key、Docker Hub、Sentry、Slack webhook。
- 删除或限制包含 secrets 的 workflow runs、artifacts 和缓存。
- 检查 GitHub audit log、npm publish、云审计日志和部署记录。
- 把第三方 Action 固定到 SHA,或换成可信替代方案。
- 给高权限 job 加
permissions、environment 审批和 OIDC。
如果你无法证明某个 secret 没有进入日志,就按已经泄露处理。CI 里的 secret 通常能继续打包、发布、部署或访问云资源,不能靠“暂时没看到异常请求”来判断安全。
上线前跑一张 Actions 检查表
发布前至少确认这些项:
- [ ] All workflows under .github/workflows are listed
- [ ] High-privilege third-party actions are pinned to full commit SHAs
- [ ] No action uses @main, @master or @latest in release/deploy jobs
- [ ] GITHUB_TOKEN permissions are explicitly set
- [ ] Test jobs use contents: read by default
- [ ] Release/deploy jobs have only required write permissions
- [ ] Secrets are not defined at workflow-level env
- [ ] Production secrets are only available to protected environments
- [ ] pull_request_target does not checkout or execute untrusted PR code
- [ ] Untrusted PR title/body/comment is not directly interpolated into shell
- [ ] Logs do not print env, .env, headers, tokens or cloud credentials
- [ ] Artifacts do not contain secrets, private config, sourcemaps or caches
- [ ] Cloud deploy uses OIDC or short-lived credentials
- [ ] Workflow file changes require code owner review
- [ ] Compromised or suspicious action usage has a rotation planGitHub Actions 的风险不在“它是不是 GitHub 官方功能”,而在 workflow 把代码、依赖、凭据、发布权限和日志放到同一条自动化链路里。能跑只是第一步;能证明第三方代码拿不到不该拿的权限、日志不会泄露 secret、外部 PR 不能触发特权路径,才适合把它接到真实发布流程。