排查伪装成安全审计的 GitHub Actions 工作流:从固定第三方 Action 提交到最小权限与令牌轮换

枫少@KillBoy
枫少@KillBoy
枫少@KillBoy
管理员
426
文章
0
粉丝
安全开发624字数 2914阅读9分42秒阅读模式
AI智能摘要
伪装成security-audit.yml的工作流常被默认为只读,但GitHub运行时不区分审计者与操作者,被攻陷的第三方action可能拿到仓库全部secrets并用GITHUB_TOKEN写入。文章从触发条件、权限配置与action固定方式入手,给出最小权限和轮换流程,并提醒固定SHA会让Dependabot告警静默。审计工作流只读吗?
— AI 生成的文章内容摘要

在代码审查中,一个名字类似 security-audit.yml 的工作流文件往往会被当作可信的基础设施:它的职责看起来是发现问题,而不是引入风险。恰恰是这种身份预设,使得攻击面被折叠进“例行审计”这四个字里——审查者关心它报告了什么,却容易忽略它实际能做什么。近期 GitHub Actions 上出现伪装成安全审计的工作流影响多个仓库的线索,把同一个问题重新摆到台前:一条工作流只要被触发,它在仓库里获得的代码只读与凭据访问能力,往往远超“审计”这个名分所暗示的范围。本文从这条信任盲区切入,先拆解工作流拿到代码与凭据的路径,再给出可复核的核查、移除与轮换流程。

伪装成安全审计的工作流从触发条件经权限声明到达凭据的路径示意

“安全审计”工作流为何容易获得信任

安全类工作流在审查中天然占便宜。它的存在理由正当,运行结果通常以报告、注释或检查结论的形式呈现,审查者下意识把它归类为只读的观察者。而 GitHub 的运行时模型并不区分“审计者”与“操作者”:GitHub 官方文档明确指出,工作流中任何一个第三方 action 被攻陷都可能非常显著,因为该 action 会获得仓库上配置的全部 secrets,并可能使用 GITHUB_TOKEN 向仓库写入。换言之,名分上的只读职责,与运行时的实际权限之间没有强制对应关系。

另一个被低估的假设是“仓库内的工作流 = 我们自己写的工作流”。实际流水线里,审计任务通常委托给第三方 action,或在 job 之间共享工作目录与上下文。GitHub 文档提示,工作流中的各个 job 可以相互影响:一个 job 可以查询后续 job 使用的环境变量、向共享目录写入后续 job 会处理的文件,甚至直接相互干预。这意味着即使唤起审计的入口是可信的,审计 task 本身仍可能成为攻击链的一环。

拆解:工作流获得代码与凭据访问的路径

触发条件决定了谁能让工作流运行

触发条件决定了工作流的暴露面。长期使用 pull_request_target、issues 或 schedule 的工作流,在拉取请求、议题事件或定时器到点时自动运行,外部输入与仓库凭据处于同一次执行中。审查时需要回答两个问题:这个触发方式是否必须存在?触发后运行的内容是否包含不可信的第三方代码或用户输入?如果“安全审计”只在发布前需要运行,那么它就不必在每次外部 PR 上被触发。

permissions 决定 GITHUB_TOKEN 的能力边界

GitHub 文档建议将 GITHUB_TOKEN 的默认权限设置为只读取仓库内容,再按工作流文件中的个别任务需要提升权限。审查应核对每个工作流的 permissions 块,尤其是显式声明了 contents: write、issues: write、pull-requests: write、actions: write 的工作流。写权限不应出现在只做读取与分析的任务上;若必须写入,应只授予需要写入的那个 job,而不是整份工作流。

secret 引用暴露了哪些凭据可被接触

仓库中任何具有写访问权限的用户都能读取仓库配置的所有 secret,因此工作流实际可触达的凭据范围,就是仓库 secret 的面板清单。审查时逐项对照:该工作流是否真的需要这些 secret?是否可以用 OIDC 换取短期云凭据,从而去掉长期密钥?敏感值是否被正确屏蔽?GitHub 文档特别要求敏感数据不得以明文存储在 workflow 文件中,并建议不要用 JSON、XML 或 YAML 这类结构化数据封装密码值,因为脱敏依赖对具体值的完全匹配,结构化封装会显著降低正确编校的概率;每个敏感值应单独设置为一个 secret。

第三方 Action 的引用方式决定代码是否可变

引用方式是供应链上最容易被放过的一环。GitHub 文档给出的结论很直接:将 action 固定到全长度提交 SHA 是目前把 action 当作不可变发布使用的唯一方式;固定到特定 SHA 有助于降低攻击者在 action 仓库中添加后门的风险。固定时应选择 action 官方仓库而不是 fork 中的 SHA,并检查 action 对仓库内容与 secrets 的处理是否与预期一致。使用 @v3、@main 这类浮动标签时,标签背后的代码可以在不改变工作流文件的情况下被替换,审查记录因此失去意义。

用最小权限与引用固定收紧暴露面

把上面四个维度收成一条基线,可以在不改变流水线功能的前提下显著降低单点被攻陷时的损失:

  • 顶层 permissions 先设为只读,再对具体 job 做最小提权;
  • 所有第三方 action 固定到全长度提交 SHA,优先来自 action 官方仓库;
  • 云端访问改用 OIDC 换取短期凭据,避免长期云密钥进入仓库 secret;
  • 把不可信构建 job 与特权发布 job 分开,两者之间只传递经过校验的产物,特权 job 不执行来自不可信产物中的脚本;
  • 尽可能减少第三方 action 的数量,缩小供应链攻击面;
  • 敏感值单独设为 secret,不在 workflow 文件中出现明文。

下面是一段最小示例,用来说明权限收敛的写法,不构成完整方案。contents: read 是基线,需要写操作的任务单独提升——注意 actions: write 只授权给确实需要读写工作流运行的 job:

permissions:
  contents: read

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<full-length-commit-sha>
      - name: Run audit tool
        uses: <org>/audit-action@<full-length-commit-sha>

  publish-report:
    needs: audit
    permissions:
      contents: read
      actions: write
    steps:
      - uses: actions/download-artifact@<full-length-commit-sha>

这段示例的边界需要说清楚:SHA 必须替换为来自 action 官方仓库的真实全长度提交 ID;actions: write 是否必要取决于报告的发布方式,如果报告只是作为 job 输出或由专门的管理员流程处理,这个写权限就不该出现在工作流里。误报的角度看,把浮动标签一键替换成一串哈希会让部分团队的 Dependabot 告警变得静默——GitHub 文档说明 Dependabot 仅为使用语义版本控制的 action 创建告警,不会为固定到 SHA 的 action 创建告警,因此需要配合依赖项审查或其它定期人工复核来保持固定值不过时。

授权仓库内的隔离核查步骤

当怀疑某条伪装成审计的工作流时,推荐把动作限定在自己有书面授权的仓库中,按准备、执行、判断三段走。

准备

  • 记录核查范围:涉及哪些仓库、哪些工作流、哪些 secret 名称与权限;
  • 导出待查工作流文件的完整版本历史,保留审查依据;
  • 在不影响业务的时间窗内进行,确认该工作流是否与发布、同步、报告依赖相关;
  • 确认事件响应负责人与回滚方式(例如临时禁用工作流而非直接删除)。

执行

  1. 检查触发条件:列出 on 块中的所有事件,标出自动触发且接受外部输入或外部代码的路径;
  2. 检查 permissions:对照每个 job 实际用到的能力,标出超出需要的写权限;
  3. 检查 secret 引用:列出工作流中引用的所有 secret,确认每个都用在该工作流内且无法缩小范围;
  4. 检查第三方 action 引用:逐个比对是否固定到全长度提交 SHA、SHA 是否来自官方仓库、作者与用途是否匹配;
  5. 检查运行日志与产物来源:确认报告与产物的生成步骤,是否包含向外部地址发送数据、是否将仓库内容写入工作区之外的位置、是否为其它 job 留下可被利用的共享文件。

结果判断

判定为可疑的门槛不是“有写权限”或“用了第三方 action”这类单一项,而是组合:自动触发 + 宽写权限 + 浮动的第三方引用 + 可触达的高价值 secret。任一组合成立,都应先隔离工作流(临时设为 disabled 或移除 on 块中的高风险触发),再进入下一步处置。

移除、凭据轮换与回归验证

隔离之后按污染面处置,而不是只删文件:

  1. 移除或重写:删除已确认恶意的工作流文件,或在保留审计功能的前提下重写为最小权限、SHA 固定的版本;若改为依赖托管 action,先审查该 action 的源码与权限需求。
  2. 轮换凭据:如果工作流运行日志中出现未脱敏的 secret,GitHub 文档建议删除日志并轮换 secret。实践上对所有可能被该工作流接触过的凭据都做轮换,包括仓库 secret、组织级 secret 以及可能被写权限影响的发布凭据。轮换后复核旧的凭据值已失效。
  3. 回收痕迹:检查工作流运行记录、产物、comment 与议题中是否有异常写入,必要时撤销由令牌产生的异常提交或分支保护修改。
  4. 回归验证:让审计流程以最小权限重新运行一次,确认所需能力可用、超出需要的写权限已不可用、第三方 action 的固定 SHA 仍然有效。回归验证不只看“跑成功”,还要看“用不到的能力确实用不上”。
  5. 留档与复评:把本轮的核查记录、判定依据和变更一并归档,并安排下一次复核的时间点。
伪装成审计的工作流从隔离到凭据轮换与回归验证的流程示意

把检查变成可复核的长期控制

单次清理不足以对抗“审计名义”这类信任盲区,需要把上面的人工判断沉淀成流水线自身的控制:

  • 仓库默认把 GITHUB_TOKEN 权限设为只读,提权改动在 PR 审查中显式标注;
  • 对工作流文件变更启用依赖项审查,让第三方 action 的引用变化在合并前可见;
  • 使用 CodeQL 的 Actions 扫描或其它工作流静态检查,将被识别出的可疑工作流模式纳入审查提示;
  • 把 SHA 固定的更新交给仍保留告警能力的机制或定期人工复核,避免固定动作导致告警静默;
  • 对高价值凭据设置轮换周期,并把工作流运行日志作为轮换触发条件之一。

这套控制的重点不在于阻断所有第三方 action,而在于让“一条工作流实际上能做什么”始终可被追问、可被复核、可被回退。平台的具体权限语义、默认设置与事件行为会随版本演进,落地前应以 GitHub 官方文档为准,并在自己的授权环境中做一次回归验证,再扩大到组织范围。

 
枫少@KillBoy
评论  6  访客  6
    • 摸鱼小霸王
      摸鱼小霸王 0

      我们仓库审计工作流的权限还真没细看,得回去查查

      • 糖宝
        糖宝 1

        固定完整SHA这点太关键了,浮动标签真容易出事

        • WoollyWinks
          WoollyWinks 0

          安全审计反而最容易被放过,这个角度确实真实

          • 阎罗使者
            阎罗使者 1

            用OIDC换短期凭据,现有流水线改动大吗

            • 印雪无痕
              印雪无痕 1

              一直以为审计工作流只读,原来权限这么大

              • 毛球小羊
                毛球小羊 0

                自动触发加宽写权限再加浮动引用,组合起来确实危险

              匿名

              发表评论

              匿名网友

              拖动滑块以完成验证