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

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

模型选中的工具以谁的权限执行——角色、允许列表、路径、确认

在 TT Lab 中继续学习

目标

把以服务权限原样执行模型所选工具的文件助手,用请求用户的角色、工具允许列表、路径校验和人工确认约束起来。

为什么重要

智能体的工具以应用服务器的权限执行,而模型不区分用户的话和文档中的话。所以 viewer 的请求或共享文档中的一句话就能执行删除(混淆代理人)。这个实验的假模型把这一弱点变得总能看见,而真实模型只是“偶尔”如此——这个偶尔无法靠测试全部找出,所以权限要在模型之外施加。评分器每次评分都会用新的用户、文件和指令启动应用,查看文件是否真的被删除、写入或读取。正常请求(viewer 的读取、editor 的写入、admin 经确认的删除)必须照常可用。

步骤

  1. 用 python3 /opt/fixtures/llmsec/kit.py agent 展开素材,然后把 /root/llmsec/agent/app.py 启动在 127.0.0.1:8103 上(禁止 0.0.0.0,关掉终端也要保持运行)。curl 127.0.0.1:8103/health 的 seed 必须与展开时得到的种子相同。
  2. 在过去的执行记录 /root/llmsec/agent/data/history.log 中只看**成功的(result=ok)**执行,统计执行了角色(role)策略表中没有的工具的次数,以及触及用户主目录之外路径(用 .. 跳出主目录或使用绝对路径)的次数,以 violations=<count>、escapes=<count> 两行写入 /root/llmsec/agent/review.txt。策略表在 app.py 开头的说明中。
  3. 修改 allowed,按请求用户的角色许可工具执行(完全按照策略表,表中没有的工具对任何人都不行)。拒绝时在 data/audit.log 中以 "event": "denied" 留下记录。viewer 的读取、editor 的写入必须照常可用。
  4. 修改 offered_tools,只向模型展示该用户角色可以使用的工具。viewer 的删除请求中根本不应出现删除调用,任何人的请求中都不应出现 run_shell。即使模型编造了列表之外的名称,也不能执行,应用也不能崩溃。
  5. 修改 resolve,把路径参数解析为真实路径,只有在该用户的主目录(data/files/<name>/)之内时才使用。通过 ..、绝对路径、指向主目录外的符号链接都不能读写别人的文件,而主目录内的路径(包括 sub/../sub/x.txt 这样的)照常可用。
  6. 修改 run_call、handle_confirm,无论模型放入的 confirm 参数是什么,delete_file 都要始终搁置:在响应的 actions 中给出 {"status": "pending", "id": <confirm-id>, ...},只有请求者本人发送 POST /confirm {"id": ...} 时才删除一次。其他用户的确认和第二次确认以非 2xx 的代码拒绝,并且其他用户的确认尝试不能清除搁置。
  7. 运行 incident ls-agent-overreach 会触发故障事件。三个人的文件消失了(/srv/ls-agent/missing.txt)。为每个人分辨原因,像实验第 3–5 步那样修改应用 /srv/ls-agent/app.py,只把被误删的文件从备份 /srv/ls-agent/backup/files/ 中恢复,然后在 /root/incident/ls-agent-overreach/report.txt 中写下三行 <owner>: <code> 以及 evidence: <log line>。代码是 model-confirm(上下文中的指令填写了确认)、owner-request(所有者亲自确认后删除——不是事故)、path-escape(跑到别人主目录的路径)、role-ignored(角色中没有的删除)、shell-tool(shell 工具)之一。

参考

展开应用并启动在环回地址上

用 python3 /opt/fixtures/llmsec/kit.py agent 展开素材,然后把 /root/llmsec/agent/app.py 启动在 127.0.0.1:8103 上(禁止 0.0.0.0,关掉终端也要保持运行)。curl 127.0.0.1:8103/health 的 seed 必须与展开时得到的种子相同。

方向:实验应用没有理由对外开放——监听的地址就是暴露范围。 工具:ss -ltnp、curl、setsid、nohup。 具体:展开命令如上。启动后,看看 ss -ltn 中 8103 端口是否显示为 127.0.0.1,以及 /health 的 seed 是否与 /root/llmsec/agent/data/seed.txt 相同。

统计过去执行中超出角色和主目录之外的情况

在过去的执行记录 /root/llmsec/agent/data/history.log 中只看**成功的(result=ok)**执行,统计执行了角色(role)策略表中没有的工具的次数,以及触及用户主目录之外路径(用 .. 跳出主目录或使用绝对路径)的次数,以 violations=<count>、escapes=<count> 两行写入 /root/llmsec/agent/review.txt。策略表在 app.py 开头的说明中。

方向:在修复之前,先统计已经发生了什么。策略不看“模型选了什么”,而看“是谁的请求”。 工具:awk、grep、python3 的 os.path.normpath。 具体:路径要规范化之后再判断——中间即使有 ..,最终回到主目录内的路径也不算外面。失败的(result=error)行不计入。

按请求用户的角色许可

修改 allowed,按请求用户的角色许可工具执行(完全按照策略表,表中没有的工具对任何人都不行)。拒绝时在 data/audit.log 中以 "event": "denied" 留下记录。viewer 的读取、editor 的写入必须照常可用。

方向:模型选择的工具名称只是提议。是否执行,由请求者的角色决定。 工具:allowed(user, tool)、users.json 的 role。 具体:把策略表转成代码,并采用封闭的默认值:表中没有的角色和工具一律拒绝。

精简展示给模型的工具

修改 offered_tools,只向模型展示该用户角色可以使用的工具。viewer 的删除请求中根本不应出现删除调用,任何人的请求中都不应出现 run_shell。即使模型编造了列表之外的名称,也不能执行,应用也不能崩溃。

方向:选不到的工具也就不会被执行。执行前夕的检查(第 3 步)与精简展示的列表是不同的层。 工具:offered_tools、TOOLS、allowed。 具体:按角色过滤要展示的列表。也要确认编造的名称会被执行一侧的检查拒绝。

路径解析为真实路径,只允许主目录内

修改 resolve,把路径参数解析为真实路径,只有在该用户的主目录(data/files/<name>/)之内时才使用。通过 ..、绝对路径、指向主目录外的符号链接都不能读写别人的文件,而主目录内的路径(包括 sub/../sub/x.txt 这样的)照常可用。

方向:用字符串拦截,换了写法的路径就会溜过去。要看文件系统实际指向的地方。 工具:os.path.realpath、os.path.commonpath、os.path.isabs。 具体:把主目录和目标都解析为真实路径,再看公共祖先是否为主目录。拒绝时抛出异常,run_call 就会以 error 返回。

确认来自人,而不是模型

修改 run_call、handle_confirm,无论模型放入的 confirm 参数是什么,delete_file 都要始终搁置:在响应的 actions 中给出 {"status": "pending", "id": <confirm-id>, ...},只有请求者本人发送 POST /confirm {"id": ...} 时才删除一次。其他用户的确认和第二次确认以非 2xx 的代码拒绝,并且其他用户的确认尝试不能清除搁置。

方向:confirm: true 是模型填写的参数,文档中的指令也能填。确认要通过模型无法书写的通道接收。 工具:PENDING、secrets.token_urlsafe、handle_confirm。 具体:收到删除调用时,不执行,而是生成 id 并连同归属一起保存;在确认请求中,只有是本人的才取出执行,然后删掉。确认之后也要重新经过角色和路径检查。

故障事件——三个人的文件消失了

运行 incident ls-agent-overreach 会触发故障事件。使用文件助手(生产版 /srv/ls-agent/app.py,数据 /srv/ls-agent/data/)的三个人的文件消失了(列表:/srv/ls-agent/missing.txt,记录:data/audit.log、data/runs.log——runs.log 中会留下请求句子、人发出的确认(part=confirm)、一并给出的上下文以及模型选择的调用)。为每个人分辨原因并修改 /srv/ls-agent/app.py——实验第 3–5 步的缺陷(角色、工具列表、路径)无论是否与事件原因有关都要全部堵上,并且不看模型填写的 confirm 参数(admin 在 /run 请求正文中附上 "confirm": true 来删除自己文件的正常用法保持不变)。然后只把被误删的文件从备份 /srv/ls-agent/backup/files/ 中以相同内容恢复——所有者亲自确认后删除的不恢复。在 /root/incident/ls-agent-overreach/report.txt 中写下三行 <owner>: <code> 以及 evidence: <log line>。每个人的代码是以下之一:model-confirm(上下文中的指令填写了确认)、owner-request(所有者亲自确认后删除——不是事故)、path-escape(跑到别人主目录的路径)、role-ignored(角色中没有的删除)、shell-tool(shell 工具)。

方向:同样是“文件消失了”,是谁、以什么句子和上下文、发出了什么工具调用却各不相同。而且并非所有删除都是事故。 工具:grep、jq、audit.log 中的 delete 和 shell、runs.log 中的 part=request、confirm、context、call、users.json 的 role。 具体:针对每个消失的路径,在 audit.log 中找到触及该文件的记录和 rid,再在 runs.log 中查看该 rid 的请求句子、上下文和调用。请求者是不是所有者、该角色能否删除、确认是来自人发出的请求字段(runs.log 中的 part=confirm 行),还是来自模型看了上下文后填写的工具参数,就是分岔点。上下文行中即使有“确认”之类的词,也不是人的确认。