一个 GitHub Issue,三个编码智能体,零权限入场

·阅读约12分钟·Evergreen Tools Team

2026 年 8 月 5 日,在 Black Hat USA 2026 上,Novee Security 的研究员 Elad Meged 公布了一项针对三大商业编码智能体的研究:一个没有任何仓库权限的账号,只要开一个 GitHub issue,就足以触达 Anthropic(Claude Code)、Google(Gemini CLI)与 OpenAI(Codex)自家仓库 CI runner 中的密钥。Cloud Security Alliance 于 2026 年 8 月 8 日发布研究简报,把其中两个漏洞编号、修复杂版本与加固建议完整记录下来(CSA,2026 年 8 月 8 日)。被测试的不是第三方草率集成,而是三家厂商自己默认、未经修改的工作流配置——研究人员随后在 100 多个公开仓库里发现了同类配置。这意味着问题不是某一家的疏忽,而是一种普遍的设计模式。

任何人都能开的 issue,就是任何人都能下的指令

任何人都能开的 issue,就是任何人都能下的指令

一、共同入口:任何人都能开的 issue

把这三次攻击放在一起看,入口完全一致:自动化把 issue 内容当作给智能体的指令,而 issue 是任何联网者都能创建的对象。当这条链路存在时,等于把一个特权执行环境的控制权交给陌生人。CSA 在简报中把这条历史线拉得更长:2026 年初的 Clinejection 事件中,仅凭一个恶意 GitHub issue 标题就实现了 CI/CD 缓存投毒,最终窃取 npm 凭据并发布被植入木马的包,安装在约 4,000 名开发者的机器上,并在这些机器上静默安装了另一个高权限 AI 智能体(CSA,2026 年 8 月 8 日)。GuardFall 与 GhostCommit 也指向同一个结论:失效往往不在模型,而在模型与真实系统之间的那一层外壳。

# .github/workflows/agent-triage.yml - never let an outsider drive the runner
name: agent-triage
on:
  issues:
    types: [opened]

permissions:
  contents: read        # no write, no packages, no id-token

jobs:
  triage:
    if: >-
      github.event.issue.author_association == 'MEMBER' ||
      github.event.issue.author_association == 'OWNER'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - run: ./run-agent.sh
        env:
          AGENT_MODE: suggest-only   # no shell exec, no auto-merge

# The disclosure's common entry point was a trigger reachable by an account
# with no repository privileges. Start by removing that trigger.
一旦智能体被攻击者控制,白名单域名就成了外泄通道

一旦智能体被攻击者控制,白名单域名就成了外泄通道

二、Claude Code:被剥掉引号之后,命令校验失效了

针对 Anthropic Claude Code 的攻击路径非常具体:其命令校验器在校验前会剥离单引号文本,于是藏在 git push --receive-pack 参数里的载荷绕过了检查、原样抵达 runner;经过两轮补丁与绕过之后,最终变体被记录为 CVE-2026-54316。NVD 的描述写得很清楚:因为 huggingface.co 是作为裸主机名预批准给 WebFetch 工具的,该域名下的任意路径——包括攻击者控制的模型仓库文件——都会自动放行,且不受允许工具列表约束。攻击者因此可以把密钥按字符编码进对公开仓库文件的请求里,用服务端的下载计数当作隐蔽的外带通道(NVD,CVE-2026-54316,修复于 2.1.163)。

# oidc.py - replace long-lived tokens with short-lived, scoped credentials
import json

def github_app_token(installation_id, repos, permissions):
    # Exchange a workload identity for a token that dies with the job.
    # Scope per repository, per permission. Never a classic PAT.
    body = json.dumps({
        "repositories": repos,
        "permissions": permissions,      # e.g. {"contents": "read"}
        "expires_in": 600,
    }).encode()
    return json.loads(post("/app/installations/%s/access_tokens" % installation_id, body))["token"]

# If a token does leak, a ten-minute read-only token is worth almost nothing
# to an attacker. A classic PAT with repo scope is worth everything.
把智能体当成特权自动化身份:最小权限,全程留痕

把智能体当成特权自动化身份:最小权限,全程留痕

三、Gemini CLI 与 Codex:一个 10.0,一个「设计如此」

Gemini CLI 的问题路径不同但后果更重:无头模式下自动信任工作区,加上允许列表只在注册时校验、执行时不校验,攻击者可以通过 Linux 的 /proc 文件系统读取父进程的环境变量,进而获得沙箱前的主机级代码执行能力,该漏洞记录为 CVE-2026-12537,CVSS v4 评分为 10.0,涉及 Gemini CLI 0.39.1 之前版本与 run-gemini-cli 0.1.22 之前的 GitHub Action(NVD,CVE-2026-12537)。OpenAI Codex 则没有获得 CVE:厂商认为「一次调用写入指令文件、后续调用加载并信任它」的多轮架构是沙箱按设计工作,而不是可修复的缺陷。CSA 的建议因此变成:使用多轮 Codex 工作流的团队要把它当成架构缺陷自行处置——拆分为独立作业、各自干净检出,或把消费上一次输出的那一轮限制在无法写入 AGENTS.md 的只读沙箱内。

# normalize.py - validate the command the shell will run, not the raw string
import shlex

def effectively_runs(raw_command):
    parts = shlex.split(raw_command, posix=True)   # quote removal happens here
    return parts

def validate(raw_command, allowlist):
    argv = effectively_runs(raw_command)
    if not argv or argv[0] not in allowlist:
        return {"allow": False, "reason": "binary not allowlisted"}
    for token in argv[1:]:
        if token.startswith("-") and any(c in token for c in "|;&$()"):
            return {"allow": False, "reason": "shell metacharacter in flag"}
    return {"allow": True, "argv": argv}

# Ask your vendor whether validators run on the raw model string or on the
# fully normalized command the shell will actually execute.

四、立刻要做的四件事

CSA 给出的即时动作有明确顺序:第一,升级到 Claude Code 2.1.163 及以上、Gemini CLI 0.39.1(或 0.40.0-preview.3)及以上、run-gemini-cli 0.1.22 及以上;第二,审计自己的哪些工作流可以被外部贡献者通过开 issue、提 PR 或留言触发,因为这是三家共同的入口条件;第三,对上述工作流能访问到的仓库密钥、GITHUB_TOKEN 作用域与模型 API Key 一并轮换——前提是补丁之前它们曾以易受攻击的配置运行过;第四,禁止在可由未认证或低权限外部输入触发的工作流中使用跳过人工审批的自动执行模式。这四步的顺序不是随意的:入口先关,凭据再换。

# proc_probe_detector.py - the credential-probing signal to alert on
SUSPECT = (
    "/proc/self/environ",
    "/proc/1/environ",
    "/proc/*/environ",
)

def alert(line, allowlisted_pids=()):
    if any(p.replace("*", "") in line for p in SUSPECT):
        pid = extract_pid(line)
        if pid not in allowlisted_pids:
            return {"severity": "high", "signal": "runner-env-probe", "line": line}
    return None

# Gemini CLI's CVE-2026-12537 was reached by reading the parent process
# environment through Linux's /proc filesystem. Almost nothing legitimate in
# a CI runner does that, so the signal has a low false-positive rate.

五、长期加固:把智能体当成特权身份

简报里最有价值的是一条原则性问题:要求厂商明确回答,命令校验究竟作用在模型产生的原始字符串上,还是作用在 shell 实际执行的、完成展开与归一化之后的命令上。因为只有后者才能可靠拦住这一类绕过。围绕这条,工程侧可落地五件事:把触发条件限制在可信作者身份上并收紧权限(代码示例 1);用短期的、按仓库按权限细分的凭据替换长期 PAT(代码示例 2);让校验发生在归一化之后(代码示例 3);把 /proc/*/environ 之类的读取行为当作高危信号告警(代码示例 4);最后用脚本把「外部可触发且持有密钥」的工作流全部清点出来(代码示例 5)。此外两条同等重要:把 issue、PR 描述、评论以及智能体会自动加载的 AGENTS.md、CLAUDE.md、.env 一律视为不可信输入;把「已批准域名」当成防火墙规则来审——因为一旦智能体被控制,白名单域名就是现成的外泄通道。

# workflow_audit.py - find every workflow an outsider can trigger
import glob
import yaml

EXTERNAL = ("issues", "issue_comment", "pull_request_target", "discussion", "workflow_run")

def audit():
    findings = []
    for path in glob.glob(".github/workflows/*.yml"):
        wf = yaml.safe_load(open(path))
        triggers = wf.get(True) or wf.get("on") or {}
        if isinstance(triggers, str):
            triggers = {triggers: {}}
        risky = [t for t in triggers if t in EXTERNAL]
        if risky:
            findings.append({"workflow": path, "triggers": risky, "jobs": list((wf.get("jobs") or {}))})
    return findings

# Rotate any secret reachable from a workflow on this list if it ran in a
# vulnerable configuration before patching. CSA recommends exactly that.

📌 常见问题 FAQ

攻击者需要仓库权限吗?

不需要。这是本次披露最关键的一点:一个没有仓库权限的账号,通过开一个 GitHub issue 就足以进入链路。研究人员测试的是三家厂商自己仓库中的默认工作流配置。

两个 CVE 分别是什么?

CVE-2026-54316 涉及 Claude Code(修复于 2.1.163),通过预批准域名 huggingface.co 的任意路径构成隐蔽外泄通道;CVE-2026-12537 涉及 Gemini CLI(0.39.1 之前)与 run-gemini-cli(0.1.22 之前),在无头 CI 平台上可通过 /proc 读取父进程环境,CVSS v4 为 10.0。

为什么 Codex 没有 CVE?

OpenAI 认为其多轮架构——一次调用可以写入指令文件,后续调用加载并信任它——属于沙箱按设计工作,而非可修补缺陷。CSA 因此建议团队自行缓解:拆分多轮为独立作业、使用干净检出,或把消费上一次输出的阶段限制在无法写入 AGENTS.md 的只读沙箱中。

有没有已被实际利用的证据?

CSA 简报指出,截至披露日,CISA 的漏洞利用跟踪数据中未发现这两个 CVE 被实际利用的确证。但这不改变整改优先级,因为同类默认配置在 100 多个公开仓库中被发现。

「已批准域名」为什么是风险点?

NVD 对 CVE-2026-54316 的描述显示,huggingface.co 作为裸主机名预批准后,该域名下任何路径都自动放行且不受允许工具列表约束,攻击者可用公开仓库的下载计数编码外带数据。因此白名单应按「完整路径与用途」而不是「域名」来定义。