TT Lab
博客 开始
学习 学习路径 课程

LLM 应用安全——从注入到输出处理

模型输出就是用户输入——流向 SQL、HTML、shell 时

在 TT Lab 中继续学习

一句话总结

模型的输出流向 SQL、浏览器、shell、数据库写入时,老的注入攻击就会经由模型卷土重来。要把模型输出与用户输入一样当作不可信的值,并在接收方施加与其上下文相符的防御(选择意图、参数、转义、不经 shell 的执行、schema 校验)。

为什么需要它

正如前面三个模块所见,模型的输出是攻击者可以书写的——文档中的指令、用户的问题、工具结果,哪里都行。可是许多应用把模型的输出当作“我们系统生成的值”,原样交给下一步:把自然语言转成 SQL 执行的报表助手、把模型回答嵌入 HTML 显示的页面、用模型决定的文件名拼出 shell 命令的导出功能、用字符串拼接模型整理的笔记的 INSERT。于是 SQL 注入、XSS、命令注入这些老问题,在“是模型写的所以没关系”的错觉下重新敞开。OWASP LLM Top 10 2025 把它列为不当的输出处理(LLM05 Improper Output Handling)。

它如何工作

流向 不要做的 要做的
SQL 读取 执行模型写的 SQL 让模型“按名称”从服务器持有的查询中挑一个,值用参数,连接用只读
HTML 原样嵌入回答 按嵌入位置的上下文(正文、属性)转义——回显问题的位置也一样
shell 拼接字符串用 shell=True 执行 使用不经过 shell 的函数(文件复制)或参数列表,名称用允许规则(字母、数字、-、_ 加扩展名)
SQL 写入 把值拼接在引号中 使用占位符,在此之前先做 schema 校验(类型、长度、引用的值是否存在)

只读连接值得单独强调。检查 SQL 字符串来过滤 DROP、UPDATE 的方式,必须无休止地追赶各种变形,而以 mode=ro 打开的连接会由引擎拒绝写入。它不是字符串检查,而是由引擎守护的边界,从这一点来说,与参数化属于同一类防御。

schema 校验就是不相信“模型遵守了格式”。应该是数字的位置可能来了字符串,应该是 300 个字符的位置可能来了 1000 个字符,应该是已有客户的位置可能来了不存在的名称。不符合就不执行,以 4xx 拒绝——不要以 500 崩溃,也不要修正后接受。

再补充一点:转义因“放在哪里”而不同。HTML 正文、属性值、JavaScript 字符串、URL 中危险的字符各不相同,所以 OWASP 的 XSS 防护指南要求按放入位置的上下文选择编码。这个实验只处理正文这一种上下文,但如果设计中要把模型回答放进链接地址或脚本内部,用同一个函数是不够的。这种位置最好一开始就不放入模型输出。

shell 也是同样的道理。加引号的函数(shlex.quote)能防住命令注入,却防不住用 .. 往上跳目录的名称。所以名称要用允许规则收窄,不需要 shell 的事(文件复制)就不经过 shell 来做。“拒绝不符合规则的”之所以比“修正危险字符后接受”更牢靠,是因为修正的一方必须知道攻击者所选输入的所有情况,而拒绝的一方只需知道允许的形态。

这门课程的假模型只要有指令,就总会原样输出攻击者的文字。在真实模型中,这样的输出偶尔、不可预测地出现。输出处理的防御是在接收方施加的,无论这个“偶尔”何时到来都给出相同结果,所以可以与模型有多乖无关地进行测试。

现场遇到的情形

典型的事故有:自然语言查询工具用模型生成的查询,原样返回了权限之外的表,也就是其他客户的数据;聊天页面原样渲染了模型回答中的标签,用户的浏览器向外部地址发出了请求;“把报表以这个名称保存”的功能执行了名称中的 shell 语法。这个实验最后的故障事件把整门课程串了起来。三个优惠券代码在外部被使用了,可能的途径有:模型写的 SQL、系统文本被提取、导出时的 shell 命令、发给合作方的记录包——其中三条是真正的路径。

加入了接收方的防御,记录也要改。原样记下模型给出的 SQL、HTML、命令的记录,本身就成了攻击字符串的仓库,如果其他工具再读取并执行这些记录,同样的问题会再发生一次。修好之后,只留下判断所需的东西,比如模型选择的意图名称、拒绝的原因、请求 id。

接下来的实验要做什么

在报表助手的查询记录中统计模型给出的危险 SQL,把查询改为选择意图、参数、只读,把页面改为转义,把导出改为不经 shell 的复制和名称规则,把笔记保存改为 schema 校验和占位符。最后分辨并堵住优惠券泄漏事件的三条路径,只更换泄漏出去的优惠券。