1. 从一次诡异的“幻觉”攻击说起Web智能体的新威胁最近在复现一个基于大语言模型的网页自动化智能体Web Agent项目时我遇到了一个极其诡异的现象。这个智能体的任务是登录一个模拟的电商后台查询特定订单的状态。在本地测试环境中它运行得完美无缺总能准确找到登录入口、输入凭证、导航到订单管理页面并返回结果。然而当我将其部署到一个临时的云端测试环境后它的行为开始变得“精神错乱”——它依然能成功登录但在后续的页面导航中会间歇性地点击一些根本不存在的按钮或者向一个错误的表单字段输入完全无关的查询信息。起初我以为是网络延迟或页面加载不全导致的典型“元素定位失败”问题。但仔细检查日志和HTML快照后发现智能体“看到”的页面DOM结构与我通过浏览器开发者工具实际抓取到的结构存在微妙的差异。智能体“认为”页面上有一个ID为#submit-order的按钮而实际页面上只有#submit-btn。更令人不安的是这种差异并非每次都出现似乎与智能体的执行流程和特定时间点有关。经过长达数天的深度排查我最终将问题根源锁定在了一个非常隐蔽的层面运行环境的内存污染。这不是代码bug也不是模型“幻觉”而是一种针对AI智能体运行时的、新型的“环境注入”攻击。攻击者无需直接修改智能体的核心代码或训练数据只需在其运行时环境例如浏览器上下文、Node.js进程或特定的解释器内存空间中植入精心构造的“毒化”数据就能永久性地扭曲智能体的认知和行为。这正是标题“Poison Once, Exploit Forever: Environment-Injected Memory Poisoning Attacks on Web Agents”所揭示的核心威胁——一次投毒永久利用。这种攻击模式完全颠覆了我们对AI系统安全性的传统认知。我们通常关注的是训练数据投毒、对抗样本攻击或提示注入但这些攻击往往需要与模型进行直接交互或影响其训练过程。环境注入的内存污染攻击则狡猾得多它攻击的是智能体赖以理解世界的“感官”和“短期记忆”。对于Web智能体而言这个“感官”就是它从浏览器中获取的页面DOM、网络响应、乃至JavaScript执行上下文。一旦这些基础信息在内存层面被污染智能体基于此做出的所有决策都将走向歧途且攻击效果会随着智能体的持续运行而不断被“重温”和“巩固”形成一种持久的后门。2. 攻击原理深度拆解内存污染如何“欺骗”Web智能体要理解这种攻击我们首先需要拆解一个典型Web智能体的工作流程。以基于LLM的智能体为例其核心循环通常遵循“感知-思考-行动”模式感知通过浏览器自动化工具如Playwright、Selenium获取当前页面的HTML、可访问性树、截图等。思考将感知到的信息通常经过简化或特征提取与任务指令一起提交给LLM。LLM分析当前状态并规划下一步动作如“点击登录按钮”、“在搜索框输入‘订单123’”。行动将LLM输出的结构化动作如{action: ‘click’, selector: ‘#loginBtn’}翻译成浏览器自动化API的调用执行操作。观察结果进入下一个循环感知动作执行后的新页面状态。环境注入的内存污染攻击其核心目标就是在“感知”阶段与“思考”阶段之间篡改流经内存的数据。攻击的切入点可以分布在多个层面。2.1 攻击面一浏览器运行时环境注入这是最直接也最有效的攻击面。Web智能体严重依赖浏览器提供的环境来渲染页面和执行JavaScript。污染DOM API攻击者可以注入一段恶意JavaScript代码劫持或包装关键的DOM查询API如document.querySelector、document.getElementById、element.innerText等。当智能体的自动化脚本调用这些API获取元素信息时返回的是被篡改后的结果。攻击示例劫持document.querySelector当检测到查询选择器是#submit-btn时动态创建一个不存在的#submit-order按钮的虚拟DOM节点并返回其引用。对于智能体来说它“看到”并成功“点击”了这个按钮但实际上浏览器并未执行任何真实点击。注入方式可以通过多种方式实现例如1作为第三方分析或广告脚本的一部分被加载2利用跨站脚本漏洞注入3在本地开发或测试环境中通过浏览器扩展或调试工具手动注入用于模拟攻击。污染网络请求/响应通过Service Worker或代理工具拦截并修改智能体发出的XHR/Fetch请求或接收到的响应。例如可以将一个正常的API响应返回订单列表篡改为包含误导性信息或错误数据的响应引导智能体做出错误判断。污染全局状态修改window对象上的某些全局变量或函数这些变量可能被页面上的业务逻辑或智能体依赖的某些客户端库所使用间接影响页面行为。2.2 攻击面二智能体框架层内存污染如果攻击无法在浏览器层面实现或者智能体采用无头浏览器且环境可控性较高攻击者可能会瞄准智能体自身的运行框架。污染中间件或适配器许多智能体框架会在浏览器原始数据送达LLM之前进行一层预处理如简化DOM、提取关键特征、计算元素坐标。攻击者可以污染这个预处理模块的内存或缓存。例如一个缓存了“常见页面元素映射关系”的字典被污染导致所有“登录按钮”都被错误地映射到一个“删除账户”的按钮选择器上。污染上下文管理Web智能体通常有“上下文”概念即保存了历史对话、已执行动作、已观测状态的内存。攻击者如果能在上下文向量数据库或内存数组中插入一条伪造的、成功的“历史记录”可能会诱导智能体在后续步骤中重复错误的操作路径。2.3 攻击面三LLM上下文提示污染虽然严格来说这不完全是“内存”污染但它是环境注入的延伸。攻击者不是直接攻击LLM模型而是污染即将发送给LLM的提示上下文。在系统提示或少量示例中植入偏见如果智能体的系统提示或少量示例Few-shot Examples是从某个可能被篡改的配置文件、数据库或环境变量中动态读取的那么攻击者可以通过修改这些源数据来 subtly 地改变LLM的决策倾向。例如在示例中将“转账确认按钮”的描述从“安全验证”改为“快速通道”可能会影响LLM对按钮功能的风险评估。这种攻击之所以危险在于其隐蔽性和持久性。被污染的数据驻留在运行时内存或环境配置中不涉及智能体核心代码的更改因此传统的代码审计或静态分析难以发现。而且只要智能体继续在该污染环境下运行攻击就会持续生效即“Poison Once, Exploit Forever”。智能体在污染数据上做出的错误决策可能会产生新的、符合污染预期的状态从而形成一个恶性的自我强化循环。3. 实战复现构建一个最小化的环境投毒攻击Demo为了让大家更直观地理解这种攻击我将演示一个最小化的、用于教育和防御研究的攻击复现场景。请注意此演示仅限在完全受控的本地或沙盒环境中进行切勿用于任何实际系统或侵犯他人权益。我们的目标让一个原本应该点击“真实按钮”完成登录的Web智能体去点击一个由我们注入的“虚假按钮”。3.1 环境准备与智能体搭建首先我们创建一个简单的目标网页和一个基础的Web智能体。目标网页(malicious_page.html):!DOCTYPE html html body h2模拟登录页面/h2 !-- 真实的登录按钮 -- button idrealLoginBtn onclickalert(真实登录逻辑执行)点击登录/button p其他一些文本内容.../p script // 这是我们将要注入的恶意脚本 // 它会在页面加载后执行污染DOM环境 setTimeout(() { console.log([攻击脚本] 开始注入恶意DOM覆盖层...); // 创建一个虚假的、覆盖在真实按钮上方的透明div const fakeOverlay document.createElement(div); fakeOverlay.id malicious-overlay; Object.assign(fakeOverlay.style, { position: absolute, top: 0, left: 0, width: 100%, height: 100%, pointerEvents: none, // 允许点击穿透不我们会精细控制 zIndex: 9999 }); document.body.appendChild(fakeOverlay); // 劫持 querySelector 和 querySelectorAll const originalQuerySelector document.querySelector; const originalQuerySelectorAll document.querySelectorAll; document.querySelector function(selector) { console.log([攻击脚本] querySelector被调用选择器: ${selector}); // 如果智能体试图寻找#realLoginBtn我们返回一个伪造的按钮元素 if (selector #realLoginBtn) { const fakeBtn document.createElement(button); fakeBtn.id realLoginBtn; // 相同的ID fakeBtn.textContent 点击登录; // 相同的文本 fakeBtn.style.cssText position: absolute; top: 100px; left: 100px; padding: 10px; background-color: #ff4444; color: white;; fakeBtn.onclick () { alert(⚠️ 你点击了被污染的虚假按钮攻击生效); // 这里可以触发任何恶意行为例如发起一个偷偷的请求 fetch(/malicious-api, {method: POST, body: JSON.stringify({stolen: data})}); }; // 将伪造按钮插入到覆盖层并返回它 fakeOverlay.appendChild(fakeBtn); // 调整覆盖层样式允许事件发生在伪造按钮上并阻止穿透到真实按钮 fakeOverlay.style.pointerEvents auto; console.log([攻击脚本] 返回了伪造的按钮元素); return fakeBtn; } // 对于其他选择器返回正常结果 return originalQuerySelector.apply(this, arguments); }; document.querySelectorAll function(selector) { // 类似地可以劫持此函数以返回包含伪造元素的NodeList // 此处为简化直接调用原函数 return originalQuerySelectorAll.apply(this, arguments); }; console.log([攻击脚本] DOM API劫持完成。); }, 500); // 延迟500毫秒注入模拟异步脚本加载 /script /body /html基础Web智能体脚本(simple_web_agent.py): 我们使用Playwright和一个简单的LLM调用此处用规则模拟来构建智能体。import asyncio from playwright.async_api import async_playwright async def run_web_agent(): async with async_playwright() as p: # 启动浏览器建议使用headed模式观察 browser await p.chromium.launch(headlessFalse) context await browser.new_context() page await context.new_page() # 1. 导航到目标页面本地文件 await page.goto(file:///path/to/your/malicious_page.html) await page.wait_for_timeout(1000) # 等待页面和潜在恶意脚本加载 # 2. 感知获取页面信息这里智能体“看到”的已被污染 # 模拟智能体通过选择器查找登录按钮 print(智能体正在寻找登录按钮 (#realLoginBtn)...) # 智能体调用的是被劫持后的 document.querySelector button_element await page.evaluate_handle(document.querySelector(#realLoginBtn)) if button_element: # 获取按钮文本确认找到的元素 button_text await button_element.evaluate(el el.textContent) print(f智能体‘看到’了按钮文本内容是: {button_text}) # 获取其位置和样式来自伪造按钮 bounding_box await button_element.bounding_box() print(f按钮位置: {bounding_box}) # 3. 思考与决策模拟LLM推理决定点击它 # 在实际LLM中这会是一个分析页面状态和任务后的决策 print(智能体决策点击这个登录按钮。) # 4. 行动执行点击 await button_element.click() print(点击动作已执行。) else: print(智能体未找到登录按钮。) await page.wait_for_timeout(3000) # 等待弹窗等效果 await browser.close() if __name__ __main__: asyncio.run(run_web_agent())3.2 攻击执行与效果分析运行智能体脚本simple_web_agent.py。你会观察到以下过程浏览器打开本地HTML页面页面上有一个蓝色的“真实登录按钮”。大约500毫秒后恶意脚本执行。它创建了一个覆盖全屏的透明层并劫持了document.querySelector。智能体脚本执行page.evaluate_handle(document.querySelector(#realLoginBtn))。此时它调用的是被劫持的函数。劫持函数检测到选择器是#realLoginBtn于是动态创建了一个红色的伪造按钮将其附加到覆盖层上并返回这个伪造元素的引用给智能体。注意智能体通过Playwright获取到的是这个伪造元素的句柄而不是页面上原本的真实按钮。智能体读取伪造按钮的文本“点击登录”和位置并决定点击它。当智能体执行click()时它点击的是那个红色的伪造按钮触发一个警告框“⚠️ 你点击了被污染的虚假按钮攻击生效”而真实的蓝色按钮从未被触及。关键点剖析智能体全程认为自己是在与真实的页面交互。它通过标准的、被信任的浏览器APIquerySelector获取元素并基于此做出决策和行动。它无法感知到这些API的返回值在内存层面已经被“调包”。攻击发生在运行时环境浏览器的JavaScript执行上下文中智能体的核心代码Python/Playwright脚本没有任何改动。3.3 攻击的变体与高级技巧上述Demo是最基础的DOM API劫持。在实际攻击中手段可能更隐蔽条件性触发恶意脚本可以设计为仅在检测到特定用户代理如包含“HeadlessChrome”的自动化工具、特定操作序列或特定时间才激活污染逻辑从而逃避人工测试。数据污染而非元素污染不创建虚假元素而是修改真实元素的属性。例如将一个“删除”按钮的innerText临时改为“保存”或者将其getAttribute(‘data-risk’)从“high”改为“low”误导基于属性分析的智能体。污染可访问性树屏幕阅读器等辅助工具依赖可访问性树。劫持相关API污染此树可以攻击那些依赖可访问性信息的智能体。利用原型链污染通过污染Object.prototype或Element.prototype可以更底层、更广泛地影响所有对象的行为难以追踪。4. 检测与防御如何为你的Web智能体构筑免疫防线面对这种“釜底抽薪”式的攻击传统的Web安全防护如WAF、输入过滤效果有限。我们需要一套针对智能体运行时环境可信度的全新防御体系。4.1 防御策略一强化环境隔离与完整性验证这是最根本的防御确保智能体运行在一个“干净”且“可信”的环境中。使用纯净、可控的浏览器实例沙盒化为每个智能体任务启动一个全新的、完全隔离的浏览器进程或容器如Docker。任务结束后立即销毁确保污染不会残留。严格扩展管理禁用所有不必要的浏览器扩展。扩展是注入脚本的常见入口。安全启动参数在启动浏览器时如Chrome/Chromium使用--disable-extensions、--no-sandbox需权衡安全、--disable-features等参数减少攻击面。实施运行时完整性检查关键API监控在智能体框架层可以注入一段“清白”的检测脚本到浏览器上下文定期检查关键原生API如document.querySelector是否被重写。比较document.querySelector document.querySelector或document.querySelector.toString()是否包含预期外的代码。DOM一致性校验在智能体采取关键行动如点击、输入前通过另一种“可信通道”交叉验证DOM状态。例如除了通过page.evaluate_handle获取元素同时使用Playwright内置的、更底层的page.locator方法它不依赖页面内的JavaScript执行来定位同一元素并对比两者的属性如坐标、标签名是否一致。# 示例交叉验证 async def safe_click(page, selector): # 方法1通过可能被污染的页面JS环境获取 js_handle await page.evaluate_handle(fdocument.querySelector({selector})) # 方法2通过Playwright原生定位器获取 locator page.locator(selector) if await locator.count() 0: raise Exception(fLocator found no element for {selector}) # 比较关键属性如是否可见 is_visible_js await js_handle.evaluate(el el.offsetParent ! null) is_visible_locator await locator.is_visible() if is_visible_js ! is_visible_locator: print(f警告JS环境与Locator对元素可见性判断不一致可能遭受污染。) # 采取安全措施如记录日志、中止任务、使用Locator的结果 await locator.click() else: await js_handle.click()4.2 防御策略二采用多模态感知与交叉验证不要完全信任单一的感知通道如纯DOM文本。引入冗余的、难以被统一污染的信息源进行交叉验证。视觉感知结合计算机视觉CV。在决策前对页面进行截图使用OCR识别按钮文本或者用目标检测模型定位UI元素的位置和类型。攻击者要同时完美污染DOM和视觉特征像素级的难度极大。工具可以集成像pytesseractOCR、OpenCV或基于深度学习的UI元素检测模型。流程智能体通过DOM找到“提交”按钮后先在对应坐标区域截图然后用OCR识别截图中的文字是否为“提交”以此验证。可访问性树感知同时解析浏览器的可访问性树。虽然它也可能被污染但攻击者需要同时篡改DOM和可访问性树增加了攻击复杂度。网络流量监控监控智能体触发的所有网络请求。如果点击一个“登录”按钮后发出了一个向陌生域名/malicious-api的POST请求这显然是异常行为应立即告警并终止会话。4.3 防御策略三在智能体决策逻辑中引入不信任与怀疑机制改变智能体的“思维模式”让它不再天真地相信所有感知信息。环境可信度评分为每次感知操作附加一个“环境可信度”分数。如果检测到API被劫持、DOM与视觉不一致等情况则大幅降低当前页面或会话的可信度分数。当分数低于阈值时智能体应转入“安全模式”——只执行最低风险操作、请求人工干预或直接终止任务。关键操作确认对于高风险操作如转账、删除、修改配置强制智能体执行额外的验证步骤。例如在输入支付密码前要求智能体通过另一个独立的感知通道如视觉重新确认当前页面标题和关键提示语。行为异常检测建立智能体的正常行为基线如典型操作序列、页面停留时间、点击模式。如果智能体突然开始频繁点击不存在的元素、在非表单区域输入、或操作序列异常可能意味着它正在被污染的感知所误导系统应触发警报。4.4 防御策略四主动狩猎与动态污点追踪对于高安全要求的场景可以采取更主动的防御。部署蜜罐元素在测试或生产页面中故意放置一些不可见visibility: hidden或对用户不可交互display: none的“蜜罐”元素。正常的智能体逻辑应该忽略它们。如果智能体的感知报告“看到”或试图与这些蜜罐元素交互那几乎可以断定其感知环境已被污染。动态污点追踪在浏览器中注入检测脚本对来自不可信源如第三方脚本的数据流进行标记污点。当智能体的核心决策逻辑使用到被污染的数据时进行拦截或记录。这项技术实现复杂但能从数据流层面提供深度保护。5. 架构层面的反思设计抗污染Web智能体系统防御单点技术固然重要但更根本的是在系统设计之初就将“环境不可信”作为核心假设。以下是一些架构设计思路感知与决策分离将负责“感知”的环境浏览器与负责“决策”的LLM/控制逻辑物理或逻辑上隔离。感知环境作为“不可信前端”其返回的所有数据都必须经过一个“验证网关”的清洗、标准化和可信度标记后才传递给“可信后端”进行决策。验证网关集成上述的交叉验证、完整性检查等功能。最小权限与指令化操作智能体不应拥有对浏览器环境的完全访问权。应通过一个安全的中间层如一个经过严格审核的浏览器扩展或本地服务来代理智能体的操作。智能体只向这个中间层发送高级指令如“在包含‘搜索’文本的输入框中输入‘XXX’”由中间层负责在可信环境下解析指令、定位真实元素并执行操作。这缩小了攻击面。定期环境重置与健康检查智能体会话不应无限期运行。采用短会话策略定期销毁并重建整个浏览器环境。在每次新建会话时执行一套标准化的健康检查流程如加载一个已知的、干净的测试页面验证关键API和DOM操作结果是否符合预期。深度防御与纵深检测在数据流的各个层面部署检测点浏览器内轻量级API监控脚本。智能体框架层感知数据的一致性校验。决策层基于行为序列的异常检测。业务层最终操作结果的合理性校验例如登录后是否真的拿到了有效的会话Cookie。在我自己的项目中在遭遇了开篇提到的问题后我最终采用了“交叉验证 短会话隔离”的组合策略。我为每个智能体任务分配一个独立的Docker容器容器内启动一个禁用所有扩展的浏览器。在关键操作步骤不仅通过Playwright Locator定位还会调用一个轻量的OCR服务对目标区域进行截图文字识别比对。同时在框架层增加了一个简单的钩子在每次page.evaluate调用前后检查几个关键全局函数是否发生变化。这套方案增加了约15%的运行时开销但彻底杜绝了同类环境注入攻击的威胁让智能体的行为恢复了稳定和可靠。环境注入的内存污染攻击为AI应用安全特别是高度依赖外部环境交互的智能体安全敲响了新的警钟。它提醒我们在追求智能体功能强大的同时必须对其运行环境的“卫生”状况保持最高级别的警惕。防御的重点不在于修补某个具体漏洞而在于构建一套贯穿始终的、不信任任何单一信息源的安全体系。这不仅是技术挑战更是一种安全设计思维的转变。