Skip to content

LLM 构建门禁的依赖身份验证绕过 ​

摘要 ​

Aviv Donenfeld 在 DEF CON 34 展示了对 OpenSearch 代码差异分析门禁的绕过。直接、编码和埋入长文本的提示词注入大多会被拦截;更有效的路径是提交一行看似合理的 Maven/Gradle 依赖。审查模型只能阅读差异和提示词,无法据此证明包名、发布者、命名空间所有权与构建期行为。构建系统随后加载依赖中的注解处理器,攻击者代码便在 CI 编译阶段运行。

这不是“模型被一句话说服”的普通提示词注入,而是依赖身份验证缺失。演讲报告最终变体在 20 次门禁测试中通过 1 次;该比例只反映特定提示词、模型与样本,不能视为通用绕过率。OpenSearch 随后把依赖变更列为高风险,但基于提示词的升级仍不能替代制品来源验证。

研究对象:门禁到底看到了什么 ​

研究对象是 OpenSearch 仓库中的 Code-Diff-Analyzer 工作流。外部贡献者提交拉取请求后,GitHub Actions 以 pull_request_target 触发,工作流通过 GitHub API 取得原始差异,构造约 50 行的安全审查提示词,再把差异经标准输入交给 Claude Sonnet 4.6。模型必须输出 JSON,为每项发现给出 critical、high、medium 或 low;脚本只要看到不低于 medium 的级别就终止后续流程。

这条门禁位于另一条具有真实权限的构建链之前:门禁放行后,Jenkins webhook 接收外部分支地址,在带有 Secret 的构建块中克隆该分支并执行 ./gradlew check。演讲展示的测试环境中,Jenkins 注入了 4 组 AWS S3 密钥和 2 组 Docker Hub 凭据。这个数量只描述研究时观察到的配置,不代表当前生产环境仍然相同。

阶段实际处理的数据当时采用的安全判断
GitHub Actions拉取请求原始差异把整段差异交给 LLM
LLM 审查文件名、增删行和约 50 行规则推断提交者是否具有恶意意图
门禁脚本模型输出的严重性medium 及以上阻断
Jenkins外部分支的完整仓库门禁已放行,继续构建
Gradle/Javac依赖制品及注解处理器按构建语义自动执行

由此出现关键的可见性断层:模型只看到“增加了一行依赖”,真正执行的类文件、META-INF/services 注册项和静态初始化代码都在远端制品内,并不在差异中。

核心创新与差异 ​

既有 Slopsquatting(幻觉包名抢注)研究主要讨论模型推荐不存在的包名后,攻击者抢注并等待开发者安装。本研究把问题推进到自动审查链:攻击者主动提交依赖变更,并利用 Maven 命名空间与域名验证之间的身份错配。最先失效的控制不是模型对一句提示的服从,而是组织没有独立证明“这个坐标由预期项目发布,且其中的构建期代码经过批准”。

演讲还区分了两个容易混淆的对象:

  • 新增 Maven 仓库地址时,门禁能把近似域名与官方 ci.opensearch.org 对比,通常识别为供应链风险。
  • 只新增一个 Maven 坐标时,模型必须凭名称猜测发布者。坐标反向域名前缀看起来合理,并不代表发布组织拥有与之对应的真实品牌或代码仓库。

门禁实现中的三个结构性问题 ​

1. 用“恶意意图”替代可验证属性 ​

原提示词明确要求模型聚焦刻意攻击(deliberate attempts),忽略普通编码错误,并根据“是否与拉取请求目的相符”判断异常。这对解释复杂代码有帮助,却不适合验证制品来源。一个坐标是否由某个组织控制、摘要是否被批准、是否声明注解处理器,都是可以用确定性数据回答的问题,不应转化为意图推理。

2. 判决直接反馈给提交者 ​

工作流把模型给出的路径、行号、严重性和解释写回可见结果。演讲者据此逐步确认哪一种表述触发了哪条规则。这不是提示词注入独有的问题;任何概率性门禁在向对手返回细粒度原因后,都会成为可迭代的判别器。

3. 门禁输入与构建输入不是同一对象 ​

门禁审查文本差异,Jenkins 则下载并执行解析后的制品。若没有在两者之间加入制品身份、摘要和构建能力检查,LLM 即使正确解释了差异,也无法证明后续执行对象与它理解的是同一个对象。

从失败的提示词注入到依赖身份混淆 ​

作者先从最直接的提示词注入开始,逐步测试了以下变体:

  1. 在 Java 字符串、注释和代码块中写入改变判决的自然语言指令。
  2. 将指令编码或拆分,并在同一差异中加入看似合理的解释。
  3. 把相关内容埋入约 4,000 行无关文本,尝试利用上下文长度和注意力分散。
  4. 不要求模型执行代码,只要求它读取环境或给出有利的严重性。
  5. 将明显代码移入注解处理器,使差异中只剩仓库地址或依赖坐标。

这些尝试并非都失败,但明显指令、编码内容和超长填充大多被原提示词中的恶意意图、混淆、供应链和异常网络访问规则识别。研究最终放弃“说服模型忽略规则”,转而让模型面对一个单靠差异无法证明真伪的命题。

命名空间混淆如何成立 ​

Maven Central 的命名空间验证允许发布者通过域名所有权证明获得反向域名形式的前缀。例如,控制 example.invalid 可以证明对相应反向坐标前缀的控制。这个流程证明的是“申请者控制某个域名”,并不证明该域名与读者心中的品牌、开源组织或已有包属于同一主体。

演讲选取了 networknt 生态作为案例:真实厂商命名空间为 com.networknt,但研究者发现可注册的近似域名能够支持另一个看似自然的反向前缀。最终差异把依赖从真实前缀视觉上改为近似前缀,且包名仍像是原项目的注解处理器。这里的关键不是某个固定域名,而是“人和模型都会把相似坐标自动归并到已知厂商”。

注解处理器为什么是一行代码执行入口 ​

Java 注解处理器是编译器插件。制品通过服务发现机制声明处理器后,javac 会在编译过程中加载对应类;类初始化和处理方法运行在构建进程中,并继承该进程可见的文件、环境变量和网络能力。因此,以下两段内容具有完全不同的可见性:

groovy
// 审查器看到的内容
annotationProcessor "org.example.invalid:review-fixture:1.0"
text
# 实际制品内部、差异中不可见
META-INF/services/javax.annotation.processing.Processor
org.example.fixture.DryRunProcessor

演讲中的制品会在静态初始化阶段读取环境变量并发送到外部端点。本站不复刻该外传实现,但保留这一结论:攻击代码不需要在被审查仓库中出现,也不需要业务源码显式调用处理器。

实验设计、变体与结果 ​

作者对每个候选差异重复运行门禁,以观察非确定性结果。演示中至少出现三类有代表性的结果:

  • 新增近似 Maven 仓库 URL 的差异被稳定识别,因为模型能把它与同文件中的官方域名直接比较。
  • 使用近似包坐标的早期变体出现约 40% 的放行;模型有时把坐标当作真实厂商发布,有时又因包名异常而判为中高风险。
  • 最终变体在 20 次测试中被拦截 19 次、放行 1 次,即放行率 5%。该次放行随后足以让 Jenkins 解析制品并触发注解处理器。

20 次样本不能给出稳定的总体绕过率,但对一次性门禁已经足够说明问题:攻击者不必获得高平均成功率,只需在允许重试、更新提交或重跑检查的环境中得到一次低严重性判决。

去武器化 PoC:验证“审查对象”和“执行对象”是否一致 ​

以下夹具仅用于自有离线仓库。测试制品不读取环境变量、不发起网络请求,只在构建目录写入固定标记。重点是验证未知注解处理器是否在安全策略批准前被执行。

java
@SupportedAnnotationTypes("*")
public final class DryRunProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> a, RoundEnvironment r) {
        try {
            Path p = Path.of("build", "DRY_RUN_PROCESSOR_RAN.txt");
            Files.createDirectories(p.getParent());
            Files.writeString(p, "no secrets; no network\n");
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        return false;
    }
}

配套回归应在隔离仓库中完成:

text
1. 向测试分支增加未知 annotationProcessor 坐标。
2. 让 LLM 输出判决,但不把判决作为唯一控制。
3. 制品代理检查坐标所有者、摘要和 Processor 声明。
4. 未批准时,构建必须在下载或加载处理器前终止。
5. 断言 build/DRY_RUN_PROCESSOR_RAN.txt 不存在。

这比验证“模型是否识别恶意代码”更接近真实控制目标。原始研究中的域名注册、公开制品投递、Secret 枚举和外部传输部分不纳入本站复现。

修复及其适用范围 ​

演讲展示 OpenSearch 后续为提示词增加了强制规则:任何依赖、包注册表或构建插件变更都必须标记为 high,并明确告知模型不得根据名字判断制品真实性。该修改能封堵演示路径,也承认了模型无法验证制品身份。

不过,这仍是把确定性规则写进概率性提示词。更稳健的实现应由工作流代码直接检查差异类型并进入独立审批分支;模型可以解释为什么变更值得审查,但不能自行降低策略要求。

防护措施与验证方法 ​

  • 将新依赖、新仓库、发布者变化、构建插件和注解处理器启用视为确定性策略事件,不交给 LLM 单独定级。
  • 对坐标所有者、制品摘要、签名、来源仓库和允许版本建立允许列表;相似名称和反向域名前缀不能继承品牌信任。
  • 解析制品元数据,显式识别 META-INF/services、Gradle 插件、安装脚本和其他构建期入口。
  • 默认关闭未经批准的注解处理器,例如使用显式处理器清单;在无长期凭据、默认拒绝出站的阶段解析新依赖。
  • 将外部分支构建与含 Secret 的发布构建拆开。门禁放行不能自动把不可信代码提升到持有发布凭据的环境。
  • 保存同一差异的多次判决,监测结果漂移;安全结论不能因模型采样或重新运行而降低。

关键结果与实际影响 ​

研究证明的是一种验证能力错配:LLM 可以发现明显危险、解释上下文和辅助人工审查,却无法从一行坐标证明远端制品的发布者与内容。真正的代码执行能力来自构建系统,而不是新增依赖行的文本外观。

影响取决于 Jenkins 是否仍持有出站网络、制品签名、仓库写入或发布权限。演讲展示了受控的凭据可见性和注解处理器执行,不应据此推断 OpenSearch 当前仍可用相同方法利用,也不应外推为所有 LLM 审查器都有 5% 绕过率。

局限与待验证问题 ​

证据来自单场会议演讲,未提供完整门禁运行参数、模型采样配置和独立复现。20 次测试不足以估计稳定成功率,OpenSearch 后续提示词修改的长期效果也没有公开测量。仍需验证制品签名、依赖锁定、内部镜像代理、显式注解处理器清单和隔离构建组合后的兼容性与拦截率。

参考链接 ​