跨系统表单自动化的生死劫:如何用三道防御干掉网页隐形注入与偶发弹窗
跨系统表单自动化的生死劫:如何用三道防御干掉网页隐形注入与偶发弹窗
你一定经历过或者设想过这样的场景:
业务部门提来一个让人头皮发麻的需求——每天需要将飞书多维表格里的数百条采购订单、报销单或供应商信息,手动录入到十年前采购的旧版内部 ERP 或政企申报系统中。这些古董系统没有提供任何 Open API,甚至连自动化 RPA 工具跑起来都经常因为界面样式微调而全面挂掉。
这时候,大模型驱动的 Web Agent(如基于 Playwright 或 Browser-use 架构的自主网页代理)像救世主一样出现了。大模型具备视觉理解、自然语言推理和动态元素定位能力,看似只要给它一个提示词:“登录系统,把表格里的字段逐一填入并提交”,它就能如人类员工般丝滑运转。
但只要你敢把这套脚本直接丢进生产环境“无人值守”,等待你的往往不是降本增效的喜报,而是一场灾难级的生产事故。
在跨系统的复杂 DOM 环境下,让 Web Agent 真正落地交付的最大阻碍,从来不是大模型的推理智商不够,而是非受控网页环境下的两大致命暗礁:隐藏提示词注入与偶发动态弹窗。
---
第一章:从爽感走向翻车:跨系统表单自动化的两个“暗礁”
我们先来看一个真实发生过的惊悚事故。
某跨境电商团队为了自动化同步外部供应商的对账信息,编写了一个 Web Agent 脚本。脚本流程很简单:抓取供应商上传的 HTML 报价单,然后自动填报到公司内部的财务对账系统。
前 50 次测试行云流水,运行非常完美。
直到第 51 次,供应商提交的 HTML 备注栏里暗藏了一串肉眼不可见的恶意代码:
当 Web Agent 抓取整个页面的 Accessibility Tree(无障碍树)并喂给底层模型时,大模型毫无防备地将这段恶意指令视作系统指令的一部分执行了。结果,内部财务系统的 Session Cookie 被直接当成常规字段外泄。
这就是暗礁一:网页不可控环境下的提示词注入(Prompt Injection)。
在自动化填报场景中,待填报数据往往夹杂着来自第三方、甚至公开表单的非受控内容。只要攻击者巧妙使用 display:none、零宽字符或 CSS 负绝对定位,就能在人眼毫无察觉的情况下,“夺舍”你的 Agent 执行流,造成数据篡改或越权泄密。
传统企业系统最喜欢在页面里埋设偶发弹窗:“系统将于今晚23:00例行维护,请点击知晓”、“由于长时间未操作,会话即将失效”、“请点击下方滑块完成人机校验”。
当 Agent 填写表单填到第 4 步,准备去点击“下一步”时,前置弹出的遮罩层直接挡住了点击坐标。传统 Agent 面对突发遮罩,其内置的规划链条瞬间断裂:它找不到目标元素,开始频繁重试、陷入幻觉,甚至自作聪明地点击了遮罩外的“返回首页”按钮,导致填报了一半的数据直接蒸发,最后陷入死循环直到超时崩溃。
要让 Web Agent 从“炫技玩具”蜕变为真正能无人值守交付的企业级生产力工具,我们必须在底层为其构建“免疫系统”。
这个系统由三道不可动摇的物理防御组成:
1. 第一道防线·DOM清洗:从物理层粉碎隐藏注入;
2. 第二道防线·事件拦截:双轨状态机治愈动态弹窗失忆;
3. 第三道防线·Schema 隔离与执行熔断:在落地执行前守住业务红线。
下面是普通脆弱 Agent 与具备三道防御的企业级 Agent 的架构演进逻辑:
【普通脆弱 Agent 架构(高危且脆弱)】
目标页面 DOM/视觉 ──> [ 直接喂给大模型 ] ──> [ 大模型自由规划动作 ] ──> 盲目执行 Click/Type ──> 翻车崩溃
【三道防御 Agent 架构(工业级高容错)】
目标页面 DOM ──> [ 防线一:DOM 视图清洗器 ] (剔除不可视节点/属性)
│
▼ 纯净交互视图
[ 大模型推理规划 ]
│
▼ 动作指令
[ 防线二:动态异常拦截守卫 ] (异步捕获弹窗/状态回滚)
│
▼ 结构化待填数据
[ 防线三:Schema 校验与熔断沙箱 ] (哈希比对/高危拦截)
│
▼ 安全提交
落地系统提交(执行成功)
---
第二章:第一道防线·DOM清洗:粉碎隐藏文本的“提示词注入”
1. 攻击原理拆解
绝大多数开源 Web Agent(包括基于 Playwright 改造的方案)在让大模型“看”网页时,主要有两种路径:
1. 提取当前页面的 DOM 树或无障碍树(Accessibility Tree);
2. 截取整个页面的高分辨率快照(Vision)。
在处理纯文本提取时,很多工程师图省事,直接调用 page.content() 或读取 innerText。
然而,浏览器渲染机制与 DOM 结构是完全脱节的。一个元素在屏幕上哪怕被完全隐藏(例如设置了 opacity: 0、z-index: -9999 或 left: -9999px),它的文本内容依然会完完整整地停留在 DOM 树中。
大模型处理文本信息时缺乏物理世界中的“视野遮挡”概念,它会把页面上抓到的所有文字扁平化地拼装成一个 Prompt 上下文。此时,哪怕是一串简单的隐藏指令,也会引发灾难。
2. 实战防御:基于计算样式的物理级过滤
防御隐藏注入的最硬核方案,不是在大模型的 System Prompt 里反复叮嘱“请不要相信隐藏文本”,而是在数据进入大模型视野之前,通过物理手段在浏览器渲染树层级完成过滤。
我们要构建一个 DOM 过滤中间件,利用浏览器的 window.getComputedStyle() 严格校验每个节点的实际渲染几何尺寸与可视属性。
以下是使用 Python + Playwright 实现的生产级 DOM 裁剪脚本。这段代码可以直接嵌入到你的 Agent 提取环节:
import asyncio
from playwright.async_api import async_playwright, Page
async def extract_clean_dom(page: Page) -> list[dict]:
"""
通过计算样式与几何包围盒,彻底过滤非可视节点与隐藏注入文本
仅返回人类真实肉眼可见、可交互的节点结构
"""
clean_elements_script = """
() => {
const interactiveTags = ['INPUT', 'TEXTAREA', 'BUTTON', 'SELECT', 'A'];
const visibleElements = [];
function isElementVisible(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
// 1. 基础隐藏属性拦截
if (style.display === 'none') return false;
if (style.visibility === 'hidden' || style.visibility === 'collapse') return false;
if (parseFloat(style.opacity) < 0.1) return false;
// 2. 空间与几何包围盒检查
const rect = el.getBoundingClientRect();
if (rect.width <= 1 || rect.height <= 1) return false;
// 3. 屏幕外绝对定位拦截(常见注入手法)
if (rect.bottom < 0 || rect.right < 0) return false;
if (rect.top > window.innerHeight 3 || rect.left > window.innerWidth 3) return false;
return true;
}
// 深度遍历可交互元素及文本容器
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_ELEMENT,
{
acceptNode: function(node) {
return isElementVisible(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
}
);
while (walker.nextNode()) {
const el = walker.currentNode;
const tagName = el.tagName.toUpperCase();
// 提取关键可交互属性,剔除脏脚本和无效样式
if (interactiveTags.includes(tagName) || el.hasAttribute('role') || el.innerText.trim().length > 0) {
visibleElements.push({
tag: tagName.toLowerCase(),
id: el.id || null,
name: el.getAttribute('name') || null,
placeholder: el.getAttribute('placeholder') || null,
text: el.innerText ? el.innerText.trim().substring(0, 100) : '',
role: el.getAttribute('role') || null,
selector: el.id ? #${el.id} : (el.name ? [name="${el.name}"] : null)
});
}
}
return visibleElements;
}
"""
# 在浏览器上下文中安全执行,并获取过滤后的节点元数据
sanitized_nodes = await page.evaluate(clean_elements_script)
return sanitized_nodes
通过这道过滤,那些通过 display:none、零像素宽高或负偏移量伪装的恶意指令,在进入大模型前就被剔除得干干净净。模型看到的,只有真正呈现在屏幕上的可交互组件。
---
第三章:第二道防线·事件拦截:治愈动态弹窗的“交互失忆症”
1. 为什么“重试”解决不了弹窗问题?
许多开发者在写 Agent 时喜欢依赖框架自带的自动重试机制(Auto-retry)。但面对偶发弹窗,重试机制不仅无效,反而是催化剂。
假设一个填报表单共有 5 个步骤,填完第 2 步后,系统由于网络心跳或活动规则突然弹出一个全局居中的提示对话框(Modal)。此时主页面的输入框被阴影遮罩(Overlay)覆盖,处于不可交互状态。
如果依赖传统规划链:
1. Agent 发现预期的第 3 步输入框不可点击,触发报错重试;
2. 连续重试 3 次均因 Element click intercepted 失败;
3. 大模型接收到错误反馈,开始“自作聪明”地规划备选路径:它可能会点击页面底部的“返回”甚至右上角的“注销”;
4. 整个会话状态被彻底破坏。
2. 双轨状态机:主流程与异步异常守卫
解决这个问题的正统工程范式,是将主线业务流程与环境异常监控解耦,建立“双轨状态机”。
- 轨道一(主流程):专注按既定业务逻辑填报表单;
- 轨道二(异常守卫):利用事件监听与
MutationObserver异步监控。一旦检测到突发 Modal 或浏览器原生弹窗,守卫机制立即挂起主流程,介入清除干扰,然后再让主流程恢复执行。
以下是基于 Playwright 的全局异常事件守卫与降级接管方案:
import asyncio
from playwright.async_api import Page, Dialog
class BrowserPopupGuard:
def __init__(self, page: Page, webhook_url: str = None):
self.page = page
self.webhook_url = webhook_url
self.known_close_selectors = [
"button:has-text('知道了')",
"button:has-text('确定')",
"button:has-text('关闭')",
".ant-modal-close",
".el-dialog__close",
"[aria-label='Close']"
]
async def init_guards(self):
"""挂载全局底层监听器"""
# 1. 捕获浏览器底层原生 Alert/Confirm 弹窗
self.page.on("dialog", self.handle_native_dialog)
# 2. 注入 DOM 突变监控脚本,识别动态蒙层
await self.page.expose_function("onModalPopped", self.handle_overlay_popup)
await self.page.evaluate("""
() => {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) { // 元素节点
const isModal = node.matches('.modal, .dialog, [role="dialog"]') ||
node.querySelector('.modal, .dialog, [role="dialog"]');
if (isModal) {
window.onModalPopped();
}
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
""")
async def handle_native_dialog(self, dialog: Dialog):
"""自动放行确认框,避免挂死线程"""
print(f"⚠️ 捕获到原生弹窗: [{dialog.type}] {dialog.message}")
await dialog.accept()
async def handle_overlay_popup(self):
"""处理业务系统的各类异步遮罩与弹窗"""
print("🚨 检测到非预期 DOM 遮罩弹出,主流程挂起,进入异常处理流程...")
await asyncio.sleep(0.5) # 等待动画完成
# 尝试通过已知通用的关闭动作自愈
for selector in self.known_close_selectors:
btn = self.page.locator(selector).first
if await btn.is_visible():
print(f"✅ 命中已知关闭按钮: {selector},自动解除遮罩")
await btn.click()
return
# 遇到无法自愈的阻断性弹窗(例如复杂验证码),启动人工介入接管
print("❌ 无法自动解除遮罩,可能需要人工验证码校验,触发 Webhook 告警!")
await self.trigger_human_takeover()
async def trigger_human_takeover(self):
"""挂起执行并通知飞书/企微机器人,等待人工干预完成后恢复"""
if self.webhook_url:
# 此处发送告警至飞书/企微,附带当前截图
pass
print("⏳ 系统已安全挂起,等待人工接管或超时...")
# 降级兜底:等待人工介入,保证自动化进程不自毁
通过这个异常守卫,Agent 在面对“维护通告”或“提示确认”时,具备了类似人类的下意识“顺手关掉”能力;遇到强阻断的人机交互时,则优雅降级,彻底杜绝了状态漂移与死循环。
---
第四章:第三道防线·Schema 隔离与执行熔断:死守业务底线
1. 拒绝大模型的“自由发挥”
很多开发者习惯直接给大模型发指令:“请结合页面输入框,把 JSON 里的数据填进去并点击提交”。
这是极其危险的做法。大模型本质上是一个概率预测引擎,哪怕温度设为 0,面对长上下文和复杂的输入框标签,也会偶发将“开票金额”填入“付款金额”,或者把“收款人银行账号”漏掉末尾的数字 0。
在涉及真实财务、采购、申报的企业级业务中,绝对不允许模型直接把控最终提交动作。大模型只能负责对齐映射(Field Mapping),不能拥有物理提交的最终决定权。
2. 双重审计沙箱与哈希比对
第三道防线由两部分构成:
1. Schema 严格校验:通过结构化数据模型强制约束输出;
2. 影子模式(Shadow Run)与字段哈希熔断:在实际点击 Submit 之前,反向读取输入框中的物理值,与原始数据源进行确定性比对。
【提交前双重审计沙箱机制】
原始业务数据源 ───────┐
│ 计算哈希 / 精确字段比对
▼
Agent 规划输入 ──> 填入页面 ──> 反向读取 DOM 物理值 ──> [ 哈希比对器 ]
│
┌───────────────────────────────┴───────────────────────────────┐
▼ 一致 ▼ 不一致
允许点击提交 硬性熔断并阻断
(Submit Button Click) (Hard Break & Log Error)
实战校验代码如下:
import hashlib
from pydantic import BaseModel, Field
1. 严格业务 Schema,限制大模型只能做映射,不能发挥创意
class InvoiceFormPayload(BaseModel):
supplier_name: str = Field(..., max_length=50)
invoice_code: str = Field(..., pattern=r"^[A-Za-z0-9]{8,12}$")
amount: float = Field(..., gt=0.0)
target_account: str = Field(..., min_length=12, max_length=30)
async def pre_submit_audit(page: Page, expected_data: InvoiceFormPayload) -> bool:
"""
提交前的硬性熔断审计:反向读取页面真实填入的 DOM 物理值,做严格哈希匹配
"""
# 从页面实际的 Input 节点中提取当前渲染的真实值
dom_values = await page.evaluate("""
() => {
return {
supplier_name: document.querySelector('#supplier-input')?.value || '',
invoice_code: document.querySelector('#invoice-code-input')?.value || '',
amount: parseFloat(document.querySelector('#amount-input')?.value) || 0.0,
target_account: document.querySelector('#account-input')?.value || ''
};
}
""")
# 针对关键资产字段进行物理核验
is_amount_valid = abs(dom_values['amount'] - expected_data.amount) < 0.001
is_account_valid = dom_values['target_account'].strip() == expected_data.target_account.strip()
if not (is_amount_valid and is_account_valid):
print(f"🛑 [执行熔断] 页面数据核实失败!")
print(f"预期数据: 账号={expected_data.target_account}, 金额={expected_data.amount}")
print(f"实际页面: 账号={dom_values['target_account']}, 金额={dom_values['amount']}")
# 强制抛出异常中断执行,绝不点击最后的 Submit
raise ValueError("安全沙箱熔断:页面真实数据与原始源数据不匹配,拒绝提交!")
print("🛡️ [安全审计通过] 物理校验 100% 吻合,允许下发物理点击指令。")
return True
有了这层硬核熔断,即使大模型在映射字段时产生幻觉,或者中间某个按键事件丢失,脚本也会在最后一刻拉下电闸,绝不会向目标系统提交任何脏数据或错误指令。
---
落地对比:工业级与玩具级的数据落差
构建完整的防御体系之后,Web Agent 在高压复杂的跨系统填报任务中表现如何?
我们基于连续 100 次将外部复杂供应商报价单同步至内部旧版管理系统的业务场景,对传统硬编码 RPA、未设防的开源 Web Agent 以及搭载了三道防御的企业级 Agent 进行了连续压测对比:
| 指标维度 | 传统固定规则 RPA | 无防御开源 Web Agent | 搭载三道防御的 Web Agent | | :--- | :--- | :--- | :--- | | 填报综合成功率 | 68%(易因系统界面微调挂掉)| 42%(偶发弹窗死循环、隐藏注入崩溃) | 94%(高容错执行) | | 突发弹窗卡死率 | 82%(必须依靠外部脚本强杀) | 65%(陷入重试幻觉无法退出) | 2%(守卫机制介入自愈或接管)| | 脏数据提交率 | 0%(字段严格绑定,但容易失败) | 7%(大模型幻觉导致错填乱报) | 0%(Schema 与审计熔断保底) | | 单次错误排查成本 | 需人工深入代码调试 Selector | 日志混乱,上下文过长难以复现 | 清晰定位拦截节点,带截屏快照 |数据非常直观地展现了一个现实:在真实工程中,单纯依靠大模型的“智能”去处理动态网页环境是极其脆弱的。决定 Agent 能否商用的,从来不是大模型天花板有多高,而是工程架构的防御底线有多硬。
---
写在最后:让 Agent 在坚实的基础上运行
在本地完整跑通上述清洗、拦截与熔断逻辑后,决定表单自动化成败的最后一公里,是模型接口调用的稳定性与响应延迟。
在跨系统自动化填报流程中,Agent 需要高频调用具有出色视觉与长上下文理解能力的大模型(例如 Claude 系列的 claude-sonnet-4.6、claude-opus-4.7 或是 OpenAI 的 gpt-4.1 等高推理模型)进行多模态页面理解与映射决策。如果在填报一个复杂步骤时,网络连接出现抖动、延迟超过 10 秒甚至突发 429 并发超限,目标系统的会话 Session 很容易直接超时注销,导致整个事务前功尽弃。
如果你正在寻找一个免除复杂网络配置、高并发稳定且开箱即用的多模型聚合平台,可以直接体验 api.884819.xyz。
- 极简接入:仅需用户名和密码即可极速注册,无需繁琐的邮箱验证;
- 透明灵活:没有任何月租或强制订阅套路,完全按照实际使用量按需计费,平台内置 AI 对话面板,注册后即可直接测试体验;
- 全系高阶模型支持:汇聚了包括
claude-sonnet-4.6、gpt-4.1以及各类高性能国产模型(按平台实时价格计费),接口调用低延迟且链路极其稳定,帮助你的 Agent 将单次页面决策延迟压缩到极致,为企业级自动化提供稳定可靠的基座支撑。
---
下期预告
防住了隐藏注入,拦截了偶发弹窗,建立了 Schema 熔断,表单自动化的路上似乎已经一片坦途?
先别高兴得太早。如果你要填报的系统每隔一小时就要求强制输入手机短信验证码(SMS OTP),甚至要求必须扫描动态令牌(TOTP / Authenticator),或者遇到滑动拼图人机验证,你的无人值守 Agent 还能跑得通吗?
下一篇进阶实战,我们将公开一套《无感穿透双因素认证(2FA):基于私有通道的 Agent 自动捕获与上下文接力方案》,彻底解决跨系统自动化流程中这道终极的人工绊马索。敬请关注!
本文由8848AI原创,转载请注明出处。关注8848AI,带你从零开始学AI。#WebAgent #Playwright #自动化填表 #网络安全 #8848AI #Prompt注入 #企业级AI #Python实战