模型选中的工具以谁的权限执行——角色、允许列表、路径、确认
目标
把以服务权限原样执行模型所选工具的文件助手,用请求用户的角色、工具允许列表、路径校验和人工确认约束起来。
为什么重要
智能体的工具以应用服务器的权限执行,而模型不区分用户的话和文档中的话。所以 viewer 的请求或共享文档中的一句话就能执行删除(混淆代理人)。这个实验的假模型把这一弱点变得总能看见,而真实模型只是“偶尔”如此——这个偶尔无法靠测试全部找出,所以权限要在模型之外施加。评分器每次评分都会用新的用户、文件和指令启动应用,查看文件是否真的被删除、写入或读取。正常请求(viewer 的读取、editor 的写入、admin 经确认的删除)必须照常可用。
步骤
- 用
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 必须与展开时得到的种子相同。 - 在过去的执行记录
/root/llmsec/agent/data/history.log中只看**成功的(result=ok)**执行,统计执行了角色(role)策略表中没有的工具的次数,以及触及用户主目录之外路径(用..跳出主目录或使用绝对路径)的次数,以violations=<count>、escapes=<count>两行写入/root/llmsec/agent/review.txt。策略表在app.py开头的说明中。 - 修改
allowed,按请求用户的角色许可工具执行(完全按照策略表,表中没有的工具对任何人都不行)。拒绝时在data/audit.log中以"event": "denied"留下记录。viewer 的读取、editor 的写入必须照常可用。 - 修改
offered_tools,只向模型展示该用户角色可以使用的工具。viewer 的删除请求中根本不应出现删除调用,任何人的请求中都不应出现run_shell。即使模型编造了列表之外的名称,也不能执行,应用也不能崩溃。 - 修改
resolve,把路径参数解析为真实路径,只有在该用户的主目录(data/files/<name>/)之内时才使用。通过..、绝对路径、指向主目录外的符号链接都不能读写别人的文件,而主目录内的路径(包括sub/../sub/x.txt这样的)照常可用。 - 修改
run_call、handle_confirm,无论模型放入的confirm参数是什么,delete_file都要始终搁置:在响应的actions中给出{"status": "pending", "id": <confirm-id>, ...},只有请求者本人发送POST /confirm {"id": ...}时才删除一次。其他用户的确认和第二次确认以非 2xx 的代码拒绝,并且其他用户的确认尝试不能清除搁置。 - 运行
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 /root/llmsec/<app>/app.py启动。放到后台,关掉终端也要保持运行(setsid nohup … > <file> 2>&1 &)。每次修改后,都要停掉正在运行的并重新启动——第 1 步的评分器会检查正在运行的应用是否由现在的 app.py 启动(这是模拟修改前的代码仍在接收请求这一运维事故的检查)。结束时也必须保持运行,第 1 步才会重新评分。 - 请求:
curl -s -H "X-Token: <token>" -H 'Content-Type: application/json' -d '{"q":"…"}' 127.0.0.1:<port>/<path>。令牌在data/users.json中。 - 评分器不使用你启动的应用,而是把 app.py 文件复制到旁边的目录,用其中新的
data/(评分器生成的随机数据)和--port,以与你相同的方式启动,并发送随机攻击。不要删掉以应用旁边的data/为基准读取数据、以--port读取端口的那些行。要修改的函数写在文件开头的说明中。察觉到正在评分而表现不同的代码判为失败。 - 假模型是
/opt/fixtures/llmsec/fakellm.py。先读一读该文件开头的说明和directives(),看看它把什么读作“指令”。 /run的正文是{"request": "...", "context": ["<shared document line>", ...]}。context 也会进入模型输入。- 事件工具:用
incident list查看可触发的事件及一行症状,用incident show <name>重新查看工单。再次触发同一事件,会以新的种子回到最初的故障状态(对象的原因也会重新确定)。 - 报告是
/root/incident/<name>/report.txt。每个对象写一行<name>: <cause-code>,最后原样附上evidence: <log line>(显示原因的一行记录,整行日志或配置,20 个字符以上)。评分器会向修好的代码重新发送新的随机攻击(不仅是这个事件的路径,也会把同一实验前面步骤的攻击作为回归测试),检查每种原因是否做了不同的善后,然后再看报告。证据行必须是事件刚发生后记录中的行,并且要与该对象的事故请求或泄漏出去的值相关联。 - 常见错误:只在路径字符串中查找
..来拦截。绝对路径和符号链接会溜过去——要解析为真实路径再比较。
展开应用并启动在环回地址上
用 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 行),还是来自模型看了上下文后填写的工具参数,就是分岔点。上下文行中即使有“确认”之类的词,也不是人的确认。