正文

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_targetworkflow_run 不处理不可信代码
日志和 artifacts不输出 token、环境变量、构建缓存和私有文件
发布凭据优先用 OIDC 短期凭据,不长期保存云密钥

先列出所有 workflow

先知道仓库里到底有哪些自动化入口。

再扫所有第三方 Action:

把结果分成三类:

如果一个第三方 Action 只做很小的事,比如列文件、拼字符串、发评论、读版本号,先判断能不能用几行 shell 或官方 CLI 代替。CI 里的依赖越少,供应链暴露面越小。

GitHub 官方actions/checkout@v4actions/setup-node@v4
平台官方docker/login-actionaws-actions/configure-aws-credentials
个人/小团队 Actionowner/action@v1
PowerShell
Get-ChildItem .github\workflows -Filter *.yml
Get-ChildItem .github\workflows -Filter *.yaml
PowerShell
rg -n "uses:\\s+[^\\s]+@[^\\s]+" .github\workflows

第三方 Action 先固定到 SHA

GitHub 文档说明,固定到完整 commit SHA 才是不可变引用。只写 @v1@v4@main@master 都可能在上游标签或分支移动后执行不同代码。

先找可移动引用:

风险较高的写法:

更稳的写法:

固定 SHA 不是“永远不升级”。更实际的做法是:

通过标准:高权限 workflow 不使用 @main@master@latest 或只有大版本号的第三方 Action。

  • 先把发布、打包、部署、签名、上传制品这些高权限 job 固定到 SHA。
  • .github/workflows/** 加 CODEOWNERS。
  • 用 Dependabot 或人工 PR 更新 Action 版本。
  • 升级 PR 里看上游 release、commit、维护者和源码改动。
PowerShell
rg -n "uses:\\s+[^\\s]+@(main|master|v[0-9]+|latest)" .github\workflows
YAML
steps:
  - uses: tj-actions/changed-files@v44
  - uses: some-owner/some-action@main
YAML
steps:
  - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
  - uses: some-owner/some-action@f1c2d3e4a5b60718293a4b5c6d7e8f9012345678

GITHUB_TOKEN 默认先降到只读

很多 workflow 没有写 permissions,开发者也就不知道 GITHUB_TOKEN 当前能做什么。GitHub 文档建议按最小权限配置 GITHUB_TOKEN,因为 Action 即使没有显式传入 token,也可能通过 github.token 上下文访问它。

先扫哪些 workflow 没写权限:

普通测试 job 可以先用只读权限:

需要写 issue、发评论、上传 package、发布 release 时,只给对应 job 加对应权限:

不要把整个 workflow 都设成写权限:

通过标准:测试、lint、typecheck 默认 contents: read;发布 job 单独拿 contents: writepackages: writeid-token: write 等必要权限。

PowerShell
rg -n "^permissions:" .github\workflows
YAML
permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
      - run: pnpm install --frozen-lockfile
      - run: pnpm test
YAML
jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      packages: write
      id-token: write
    steps:
      - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
      - run: pnpm build
YAML
permissions: write-all

secrets 只给真正需要的 job

secrets 最常见的错误是“为了方便所有 job 都能用”。测试 job、lint job、PR 检查 job 通常不应该拿生产部署密钥。

先扫 workflow 里用到了哪些 secrets:

再按用途归类:

不要把 secrets 放在 workflow 顶层 env

更稳的是只放到需要的 step:

如果是生产部署,使用 GitHub Environments 和 required reviewers,让 job 进入生产环境前需要审批。这样可以避免普通分支或误触发 workflow 直接拿到生产 secret。

NPM_TOKENnpm publish job
DOCKERHUB_TOKENimage push job
AWS_ACCESS_KEY_ID部署 job,最好迁移到 OIDC
SSH_PRIVATE_KEY受控部署 job
SENTRY_AUTH_TOKENsourcemap 上传 job
PowerShell
rg -n "secrets\\.[A-Z0-9_]+|env:\\s*\\$\\{\\{\\s*secrets\\." .github\workflows
YAML
env:
  NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
YAML
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - run: pnpm publish --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

外部 PR 不要拿特权上下文

pull_request_targetworkflow_run 很容易被误用。GitHub 文档明确提醒,不要让这些特权触发器 checkout 或执行来自不可信 PR 的代码。它们可能有仓库写权限,也可能访问 secrets。

先扫危险触发器:

危险组合:

这个组合会在特权上下文里执行外部 PR 的代码。更稳的分工是:

如果你必须用 pull_request_target,先确认 workflow 不执行 PR 分支代码、不读取 PR 里上传的脚本、不把 PR 标题/正文直接拼进 shell。

跑外部 PR 测试pull_request,不给 secrets
给 PR 加标签或评论pull_request_target,不 checkout 外部代码
构建完成后发布workflow_run,只信任受控 workflow 的产物
部署生产workflow_dispatch 或受保护分支 push,加 environment 审批
PowerShell
rg -n "pull_request_target|workflow_run" .github\workflows
YAML
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

不可信输入不要直接拼进 run

PR 标题、issue 内容、commit message、分支名、用户名都可以被攻击者控制。把这些值直接拼进 shell,可能变成脚本注入。

危险写法:

更稳的写法是先放到环境变量,再用 shell 变量处理:

如果这一步要调用 AI Agent、代码生成工具或自动修复工具,不可信输入还可能进入 prompt。此时还要限制工具权限,不要让它在同一个 job 里拿到发布 token、云凭据或仓库写权限。

YAML
- name: Check title
  run: |
    echo "${{ github.event.pull_request.title }}"
YAML
- name: Check title
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: |
    printf '%s\n' "$PR_TITLE"

日志和 artifacts 要按泄露处理

tj-actions/changed-files 事件里,secrets 的风险路径就是 workflow 日志。很多团队只盯着源代码和 secrets 配置,却忘了日志、artifacts、缓存和测试报告也会被下载或长期保留。

先扫明显会打印环境变量的命令:

再查上传 artifacts 的步骤:

高风险输出包括:

通过标准:workflow 日志里不能出现 secret 原文;artifacts 不包含 .env、私钥、token 配置、生产配置、构建缓存和上传文件原文。

  • 完整环境变量。
  • .env.npmrc.pypirc、kubeconfig、SSH key。
  • 测试失败时的完整 HTTP headers。
  • 打包后的 sourcemap,包含源码和内部路径。
  • Docker build logs 里的 build args。
  • AI Agent 的执行日志、prompt、工具调用结果。
PowerShell
rg -n "printenv|env|set|cat\\s+\\.env|echo\\s+\\$\\{|Write-Host\\s+\\$" .github\workflows
PowerShell
rg -n "actions/upload-artifact|path:" .github\workflows

发布凭据优先换成 OIDC

长期云密钥放在 GitHub Secrets 里,一旦 workflow、第三方 Action 或日志出问题,就要按泄露处理。GitHub OIDC 文档说明,workflow 可以向云厂商换取短期 token,减少长期云凭据复制到 GitHub 的需要。

发布 job 可以长这样:

检查云侧 trust policy 时,至少限制:

通过标准:生产部署不依赖长期 AWS_SECRET_ACCESS_KEYAZURE_CLIENT_SECRETGCP_SERVICE_ACCOUNT_KEY 这类静态密钥;必须保留时,限制到单 job、短保留、可轮换、可审计。

仓库只允许 repo:org/repo
分支只允许 refs/heads/main
环境只允许 environment:production
workflow只允许指定 workflow 文件
权限只允许部署需要的云资源
YAML
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

workflow 文件要有人审

.github/workflows/** 的改动不只是“CI 配置”。它可能改变仓库权限、发布路径、secret 使用方式和部署目标。

加 CODEOWNERS:

再配分支保护:

通过标准:任何新增 third-party Action、secret、permissions: writepull_request_target、部署 job,都必须有人看过。

workflow 改动必须 review防止悄悄加恶意 job
禁止普通成员修改 Actions 设置防止放开第三方 Action 策略
发布环境 required reviewers防止误触发生产部署
禁止 Actions 创建/批准 PR防止自动化绕过人工审查
Text
.github/workflows/** @your-org/platform-team @your-org/security-team

可以用工具扫一遍 workflow

人工检查之外,可以跑 workflow 安全扫描。

OpenSSF Scorecard 可以检查仓库安全实践:

zizmor 可以静态分析 GitHub Actions workflow:

工具发现的问题不能直接替代人工判断。它们适合抓这些常见风险:

如果工具提示和业务需要冲突,先把例外写清楚:哪个 job、为什么需要、谁批准、失败后怎么轮换凭据。

  • Action 没固定到 SHA。
  • GITHUB_TOKEN 权限过宽。
  • pull_request_target 使用危险。
  • 不可信输入拼进 shell。
  • secrets 暴露到过宽的 job。
  • workflow 复用边界不清楚。
PowerShell
scorecard --repo=github.com/your-org/your-repo
PowerShell
zizmor .github\workflows

已经用过可疑 Action 怎么办

如果发现项目用过被攻陷 Action,或者某次 workflow 日志里出现了 secret,不要只把 uses: 改掉。

按泄露处理:

如果你无法证明某个 secret 没有进入日志,就按已经泄露处理。CI 里的 secret 通常能继续打包、发布、部署或访问云资源,不能靠“暂时没看到异常请求”来判断安全。

  • 先停用相关 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。

上线前跑一张 Actions 检查表

发布前至少确认这些项:

GitHub Actions 的风险不在“它是不是 GitHub 官方功能”,而在 workflow 把代码、依赖、凭据、发布权限和日志放到同一条自动化链路里。能跑只是第一步;能证明第三方代码拿不到不该拿的权限、日志不会泄露 secret、外部 PR 不能触发特权路径,才适合把它接到真实发布流程。

Text
- [ ] 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 plan

参考来源

Harden-Runner detection: tj-actions/changed-files action is compromisedStepSecurityCVE-2025-30066: tj-actions changed-files allows remote attackers to discover secrets by reading actions logsGitHub Advisory DatabaseSecure use referenceGitHub DocsUse GITHUB_TOKEN for authentication in workflowsGitHub DocsOpenID ConnectGitHub DocsUsing secrets in GitHub ActionsGitHub DocsOpenSSF ScorecardOpenSSFzizmor: Static analysis for GitHub Actionszizmor

相关文章

npm 供应链攻击后,开发者要怎么检查依赖、lockfile 和 CI 密钥工程实践 / 约 13 分钟Docker 镜像里最容易泄露什么:.env、构建缓存、SSH key 和多阶段构建工程实践 / 约 12 分钟日志不是越详细越好:开发者如何避免把 token、手机号、订单和 prompt 打进日志工程实践 / 约 12 分钟API 接口上线前要查什么:鉴权、越权、限流、幂等和错误返回工程实践 / 约 13 分钟Vibe Coding 小应用上线前要查哪些安全坑智能编程 / 约 12 分钟AI Agent 权限怎么设:哪些命令能自动跑,哪些必须人工确认智能编程 / 约 13 分钟AI Coding 跑得越快,GitHub 和 CI 为什么先开始吃不消智能编程 / 约 10 分钟装过 codexui-android 后,Codex 登录凭据可能泄露怎么办错误日志 / 约 8 分钟

作者信息