越权工具与数据外泄
正在读取学习记录…
订单 Agent 帮 A 租户的客服查看一张订单。租户表示一组相互隔离的客户或组织。正常情况下,客服只能读 A 租户的订单,并把商品摘要返回公司内部页面。
越权工具调用是执行了当前身份无权执行的动作;数据外泄是数据流到了无权接收的位置。两者可能同时发生,例如“读订单”工具拿到 B 租户数据,再把结果发往外部地址。也可能分别发生:工具有读取权,但输出字段或目的地超出了任务需要。
最小权限要求权限范围与任务匹配。对订单摘要而言,允许读订单、限定租户和输出位置,比给 Agent 一套能访问所有数据的共享管理员身份更容易控制。
把一次工具调用拆成四个问题
会话是应用保存当前用户状态的对象。示例 Session 的用户、租户、工具集合和目的地集合来自应用侧,而不来自模型生成的 JSON。工具参数只包含订单编号和目的地;它们仍需验证。
read_order 先看工具是否可用,再查对象是否存在、对象属于哪个租户、结果送到哪里,最后选择需要返回的字段。这样,未通过租户校验的记录不会先被格式化成邮件正文。字段投影是从记录中选择必要字段,示例仅返回商品,邮箱留在输入记录中。
权限要在每次读取与产生副作用时检查。缓存也需要带租户和用户范围:把缓存键仅写成订单编号,可能使另一个用户得到前一位用户的结果。数据库查询可将租户作为必要条件,下游服务同样保留身份上下文。
| 校验 | A 客服的正常值 | 失败后结果 |
|---|---|---|
| 工具 | read_order | 拒绝调用 |
| 对象归属 | 订单属于 A | 不返回订单字段 |
| 输出目的地 | internal | 不交给外部发送工具 |
| 字段范围 | 商品摘要 | 排除邮箱等无关信息 |
在空目录保存并运行两个文件
在 PowerShell 输入以下命令创建练习目录,并进入目录。目录创建好后,在编辑器中新建下面两份文件并保存到这个目录。
New-Item -ItemType Directory -Force l8-privilege
Set-Location l8-privilegemacOS 终端:
mkdir -p l8-privilege
cd l8-privilege先保存 privilege_demo.py。它包含前面解释的目的地校验以及正常和边界输入。
"""在读取数据前校验租户、工具和输出目的地。"""
from dataclasses import dataclass
@dataclass(frozen=True)
class Session:
user: str
tenant: str
tools: frozenset[str]
destinations: frozenset[str]
# 无真实客户信息;两条记录用于观察租户边界。
RECORDS = {
"order-a": {"tenant": "A", "item": "课程笔记", "email": "a@example.test"},
"order-b": {"tenant": "B", "item": "练习册", "email": "b@example.test"},
}
DESTINATION = "internal"
def read_order(session, order_id, destination):
# 所有拒绝都发生在输出客户字段之前。
if "read_order" not in session.tools:
return "拒绝:工具权限"
record = RECORDS.get(order_id)
if record is None:
return "拒绝:对象不可用"
if record["tenant"] != session.tenant:
return "拒绝:租户不匹配"
if destination not in session.destinations:
return "拒绝:输出目的地"
# 按任务需要投影字段:摘要不需要邮箱。
return "订单:" + record["item"]
def main():
session = Session("reader-a", "A", frozenset({"read_order"}),
frozenset({"internal"}))
print("正常读取 ->", read_order(session, "order-a", DESTINATION))
print("另一租户 ->", read_order(session, "order-b", "internal"))
print("外部输出 ->", read_order(session, "order-a", "external"))
if __name__ == "__main__":
main()再保存 test_privilege.py。测试通过程序的公开函数检查正常路径与拒绝路径;测试文件和主程序放在同一目录,才能按文件名导入。
import unittest
# 固定良性输入检查正常、拒绝和边界路径,不连接外部服务。
from privilege_demo import Session, read_order
class PrivilegeTests(unittest.TestCase):
def setUp(self):
self.session = Session("u1", "A", frozenset({"read_order"}),
frozenset({"internal"}))
def test_normal(self):
self.assertEqual(read_order(self.session, "order-a", "internal"), "订单:课程笔记")
def test_tenant(self):
self.assertEqual(read_order(self.session, "order-b", "internal"), "拒绝:租户不匹配")
def test_destination(self):
self.assertEqual(read_order(self.session, "order-a", "external"), "拒绝:输出目的地")
def test_tool(self):
user = Session("u1", "A", frozenset(), frozenset({"internal"}))
self.assertEqual(read_order(user, "order-a", "internal"), "拒绝:工具权限")
def test_missing(self):
self.assertEqual(read_order(self.session, "missing", "internal"), "拒绝:对象不可用")
def test_minimize_output(self):
self.assertNotIn("@", read_order(self.session, "order-a", "internal"))
if __name__ == "__main__":
unittest.main()返回同一个终端,先列出当前目录内容,确认两个文件名都以 .py 结尾,然后运行主程序。
ls
python -X utf8 privilege_demo.py程序输出:
正常读取 -> 订单:课程笔记
另一租户 -> 拒绝:租户不匹配
外部输出 -> 拒绝:输出目的地第一行返回 A 租户的商品摘要。第二行命中租户不匹配,未输出 B 租户的商品。第三行记录仍属于 A,但 external 不在目的地集合中,因此同样没有返回数据。允许一个工具名只回答了“能调用哪种功能”,后面的对象和目的地检查决定了具体数据能否流动。
运行测试:
python -X utf8 -m unittest -v test_privilege.py末尾应显示 Ran 6 tests 和 OK。若提示找不到模块,先用 dir 核对目录与文件名。
改一处,观察决定怎样变化
把文件顶部 DESTINATION = "internal" 改成 DESTINATION = "external",再运行主程序。第一行变为 正常读取 -> 拒绝:输出目的地。订单与身份没有变化,变化发生在输出通道。将常量改回 "internal" 后,第一行恢复商品摘要。
python -X utf8 privilege_demo.py恢复原值后,重新运行测试。
共享工具服务里的排查顺序
| 现象 | 可能缺口 | 具体检查 |
|---|---|---|
| 同一工具能读到其他租户 | 只校验工具名 | 将对象归属与会话租户比较,查询也带租户条件 |
| 摘要里出现邮箱 | 整包序列化记录 | 将输出改为字段白名单,确认下游日志不存原包 |
| 身份正确但外部发送成功 | 读取与发送权限混为一组 | 在发送入口独立校验收件人、通道和数据级别 |
| 客服读不到自己的订单 | 会话租户或对象元数据错误 | 查看两处可信记录,避免从模型参数补租户 |
生产系统需要兼顾报错与枚举风险:面对未知对象和无权对象,外部返回可以统一为“对象不可用”,详细拒绝原因留在受控审计里。示例将原因分开,便于跟做观察。权限撤销也要影响缓存和长任务,不能依赖任务开始时的一次判断。
OWASP 授权指南建议默认拒绝并对每次请求检查权限。实际服务还应在数据库、工具执行与输出侧落实相同的业务边界。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 先判断边界
关于“越权工具与数据外泄”,哪种做法更稳妥?
对应知识点:L8-privilege
简答题 · 解释失败
写出一个失败输入并说明记录字段。
对应知识点:L8-privilege
代码题 · 边界测试
补充正常、边界、失败三组输入和预期结果。
对应知识点:L8-privilege
场景题 · 结果未知
证据不足时如何设计状态和人工接管?
对应知识点:L8-privilege
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。