AI代理供应链安全2026:llms.txt投毒防御实战
2026年8月27日,Ars Technica 披露了一起针对 AI 代理的供应链攻击:100 多个网站的文档文件(llms.txt 与 llms-full.txt)引用了危险的可执行内容,编码代理访问这些网站时会自动安装。研究人员扫描了 6,214 个活跃域名、8,265 个 llms.txt 文件,发现其中 120 个指向未注册的代码包;注册这些包名后,一小时内就有 Fortune 500 公司回连。研究者 Alon Hertz 一针见血:「信任模型已经坏了——代理把供应商文档当作 ground truth,监督它们的人类也不质疑。」本文给出四层防御的完整代码实现。
1. 攻击原理:代理把文档当 ground truth
llms.txt 是网站为 AI 提供机器可读摘要的新兴约定,相当于给 AI 的 robots.txt。攻击者利用的正是代理对文档的盲目信任:恶意文件里写着「Installation: pip install [未注册包名]」或「npm install [未注册包名]」。因为包名尚未注册,攻击者可以先注册它,然后托管勒索软件或任何恶意载荷。当有 shell 权限的编码代理把这些文件当作权威安装文档时,就会下载并执行。
// Layer 1: Parse and validate every llms.txt before trusting it.
// Researchers found 120 poisoned files across 100+ corporate sites;
// packages were unregistered, so attackers could register them and
// ship ransomware through "pip install" instructions.
import { fetch } from "undici";
interface LlmsTxtEntry {
link: string;
title: string;
installHints: string[];
}
export async function auditLlmsTxt(url: string): Promise<LlmsTxtEntry[]> {
const res = await fetch(url);
const text = await res.text();
const entries: LlmsTxtEntry[] = [];
for (const line of text.split("\n")) {
if (!line.startsWith("- ")) continue;
const [link, ...rest] = line.slice(2).split(": ");
entries.push({
link,
title: rest.join(": "),
installHints: collectInstallHints(line),
});
}
return entries;
}
function collectInstallHints(line: string): string[] {
return (line.match(/(?:pip|npm|brew|apt) install[^\n]*/g) ?? []);
}2. 第一层防御:解析并审计 llms.txt
第一条防线是永远不要自动信任 llms.txt 里的安装指令。写一个审计器:抓取文件、解析条目、提取所有 pip/npm/brew/apt install 提示,然后对每个包名向公共注册表做注册状态检查。扫描成本很低——几千个 HTTP 请求加少量注册表查询,只占代理实际工作的一小部分时间。如果你的代理要访问外部文档,让审计器先跑一遍,把可疑条目标记出来而不是直接执行。把审计器做成代理启动序列的一部分,而不是事后补丁:每个新项目、每个陌生依赖、每份出现在工单里的厂商文档都走同一流程。时间久了你会积累一份已知安全包名基线,审计会越来越快,第四层的允许列表也会更有用。
// Layer 2: Pin every dependency with a hash. Never "install latest".
// The poisoned files pointed to unregistered PyPI/npm names; an attacker
// registers the name and hosts anything they want.
// pip install with hash pinning:
# requirements.txt
requests==2.32.3 --hash=sha256:8d2c0d1f9a...
pydantic==2.9.2 --hash=sha256:1a3f5c2e9b...
# pip install --require-hashes -r requirements.txt
// npm with lockfile verification:
// 1. Commit package-lock.json
// 2. Run CI check:
// npm ci --ignore-scripts --audit
// 3. Reject lockfile drift in code review.3. 第二层防御:哈希固定依赖
被投毒的文件指向未注册的包名,而「install latest」语义正是这类攻击的温床。pip 的 --require-hashes 模式、npm 的 lockfile 加 npm ci 是行业标准答案:锁死版本和哈希,任何未固定依赖都直接失败。关键是把 lockfile 的变更纳入代码审查,拒绝静默漂移。
// Layer 3: Never let an agent run arbitrary shell commands.
// Run agent tool calls inside a sandbox with no network + no persistence.
// Example: Docker with seccomp + no network + read-only FS.
services:
agent-sandbox:
image: node:22-slim
network_mode: "none"
read_only: true
tmpfs:
- /tmp:size=100m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
volumes:
- ./workspace:/workspace:rw
command: ["node", "/workspace/agent.js"]
# And gate every install command through a policy proxy:
{
"policy": {
"allow_install": ["npm ci", "pip install --require-hashes -r requirements.txt"],
"block_install": ["npm install", "pip install", "curl | sh"],
"allow_network": ["api.github.com", "registry.npmjs.org"],
"block_network": ["*"]
}
}4. 第三层防御:沙箱化 shell 执行
编码代理需要 shell,但 shell 不该裸奔。用 Docker 加 seccomp、无网络、只读文件系统、丢弃全部 capabilities,把代理限制在临时工作区里。这套打法安全行业对不可信代码用了十年——唯一的新变量是,不可信代码现在通过聊天界面进来。更彻底的做法是在工具层加策略代理:拦截每条安装命令,命中允许列表才放行,其余全部阻断并告警。别忘了报告里关于父进程链的细节:研究者的信标精确追踪到是哪个代理发起了每次安装。你的审计日志也应该能回答同样的问题——哪个代理、哪个会话、哪条命令——而不需要做取证调查。
// Layer 4: Allowlist the registries and packages agents may touch.
// Agents treat vendor docs as ground truth — so the trust model
// has to be enforced in code, not in the prompt.
const ALLOWED_PACKAGES = new Set([
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
]);
async function beforeInstall(cmd: string, cwd: string) {
const parsed = parseInstall(cmd);
if (!parsed) return { allow: true };
const key = `${parsed.name}@${parsed.version}`;
if (!ALLOWED_PACKAGES.has(key)) {
await notifySecurity(`Blocked install: ${key}`);
return { allow: false, reason: "not-on-allowlist" };
}
return { allow: true };
}
// Attach to the agent's tool layer:
const safeTool = wrapWithPolicy(agent.tools.shell, {
before: beforeInstall,
auditLog: (entry) => appendAudit(entry),
});5. 第四层防御:注册表与包允许列表
允许列表是唯一能真正对抗「包名被抢注」的机制。维护一个团队级允许列表(包名+精确版本),代理执行任何安装前先查表:不在表里就阻断、通知安全团队并记录审计日志。把策略挂在代理的工具层上,而不是写在提示词里——提示词可以被文档内容污染,代码策略不会。
// Agent configuration: the hygiene baseline every team should adopt.
// From the Ars Technica report: a Fortune 500 phoned home within an hour
// of a researcher registering a poisoned package name.
agent:
shell:
enabled: true
sandbox: docker
policy_file: ./agent-policy.json
installs:
mode: allowlist-only
require_hashes: true
network:
egress: registry-only
docs:
llms_txt:
enabled: false # never auto-follow install hints
audit: true # but log what the site asked for
audit:
log_all_shell: true
alert_on:
- "pip install"
- "npm install"
- "curl.*\|.*sh"
- "chmod +x"
human_approval:
required_for: [install, network_egress, filesystem_write_outside_workspace]6. 团队落地清单
至少做到:禁止代理自动跟随 llms.txt 安装指令(改为审计模式);安装一律 require-hashes;shell 走沙箱;网络出口限制为注册表;所有安装类命令需要人工审批;审计日志全量记录 shell 与网络事件。从零开始的话,按顺序落地四层——审计、固定、沙箱、允许列表——把每一层当成独立的可部署里程碑,而不是一次性大工程。报告中最触目惊心的数字——注册恶意包名后一小时内 Fortune 500 回连——说明这不是理论风险,是当下正在发生的攻击。而修复方案大多是朴素的工程:校验、固定、沙箱化,加上一条纪律——绝不让代理逐字执行文档。
📌 常见问题 FAQ
llms.txt 是什么?
llms.txt 是网站为 AI 代理提供机器可读摘要的新兴约定,相当于给 AI 的 robots.txt,列出站点的内容结构和链接。Cloudflare 等大厂也在维护自己的 llms.txt。
为什么代理会执行 llms.txt 里的安装指令?
编码代理有 shell 权限时,会把文档当作权威设置说明。投毒文件里写「pip install [未注册包名]」,代理就照做。研究者 Alon Hertz 称之为「信任模型坏了」。
攻击者怎么利用未注册的包名?
文件里引用的包名没有在 PyPI/npm 注册,攻击者可以抢先注册,然后在包里托管勒索软件或任何恶意载荷。凡是执行安装的代理都会中招。
npm 的 lockfile 够安全吗?
够用但要看怎么用。必须用 npm ci(不是 npm install)、提交 lockfile、在 CI 里跑 --ignore-scripts 和 --audit,并把 lockfile 变更纳入代码审查。
小团队没有专门安全团队怎么办?
从最低限度开始:关闭代理自动安装(改为审计模式)、安装 require-hashes、shell 走 Docker 沙箱、网络出口限制为注册表。这四条不需要安全团队也能落地。