ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

多智能体平台共享工具权限隔离实战:从越权到资源级ACL

多智能体平台共享工具权限隔离实战:从越权到资源级ACL “你们的工具谁都能调吗”这是我们多智能体平台上线第一周运维同事问我的第一句话。当时我有点不服气觉得工具网关都做了鉴权怎么叫谁都能调。结果他当场用 curl 调了一个我们自己内部测试工具成功删了一条测试订单。我愣了几秒然后意识到问题出在哪那个工具能跑通是因为它只校验了“请求里有没有合法 token”但 token 背后是哪个租户、哪个用户、有没有权限操作这条订单根本没有进入任何判断逻辑。这就是多智能体系统里最典型的越权场景。智能体越强大它能操作的资源就越多如果共享工具这层不把权限隔离做好一个低权限的客服智能体完全可以借着“帮用户查单”的名义把别人的订单删了。更麻烦的是多智能体系统里工具调用不是单纯的人机交互而是智能体之间、智能体与系统之间的大量自主调用身份在链路里稍一丢失越权范围就会被成倍放大。这篇文章不聊概念只聊落地。我会从权限为什么容易失控讲起把三种主流隔离方案掰开揉碎然后给出我实际在用的整套工具网关 资源级 ACL 方案附带可以直接抄的代码和测试用例思路。适合正在做多智能体平台、Agent 工具编排或者传统系统要接入智能体能力的后端同学。1. 多智能体共享工具为什么会成为越权重灾区1.1 智能体比传统服务更容易越权根因在于“调用链路上身份断了”单体应用时代权限链路非常清晰用户登录拿到 token网关解析 token后端业务代码根据 token 里的 user_id 和角色做控制。整个链路里身份信息是随着 HTTP 请求头一路传递的后端永远不会怀疑“当前用户是谁”。多智能体系统把这条链路彻底打乱了。现在一次工具调用的完整路径是用户 → 编排层(智能体) → 大模型(LLM) → 工具调用 → 工具网关 → 业务系统问题就出在中间夹了一个大模型。大模型不会像传统网关那样原样透传身份它只负责根据用户的自然语言指令生成“该调用什么工具、参数是什么”。而工具调用这一跳往往是异步的、多跳的用户和最终工具之间的身份关系会被层层剥掉。拿企业采购助手来举例。业务方接入了采购申请审批智能体和库存查询智能体两个智能体都共享同一个inventory.query工具。库存工具的业务逻辑是传入商品 SKU返回库存数量。从工具接口的设计上看它根本不知道调用方是一个普通采购员还是库存管理员更不知道这个采购员所属的是华东大区还是华南大区。一旦工具内部直接查询了全量库存表任何一个智能体都能通过这个工具拿到不该看的区域库存数据。还有一个更隐蔽的问题智能体在执行多步任务时会在内部做工具的串联。比如“查一下某供应商的价格并生成比价单”这个任务智能体可能先调用supplier.query再用返回值去调用report.generate。如果第一次调用时身份上下文没有被透传到第二次调用那么report.generate就会被当成一个匿名或系统级请求权限校验等于形同虚设。1.2 三类典型越权场景拆解我在实际排查中把多智能体共享工具里最常见的越权场景归成了三类每一类都对应一套独立的防护重点。水平越权同租户不同用户之间这是最容易发生的一类。同一个企业租户里有多个部门、多个用户每个用户都有自己的角色和可见范围。A 部门的智能体如果可以通过枚举文档 ID 去调用document.read工具读取 B 部门同事创建的方案文档这就是典型的水平越权。危险点在于用户都在同一个租户里工具层很难直接通过租户维度去拦截必须把校验下沉到资源的所有者维度。垂直越权低权限角色调用高权限能力这类往往发生在工具接口设计得太“粗”的时候。比如一个客服智能体本来只需要查询订单状态和修改备注但order.update工具对应的后端接口同时支持取消订单和修改价格。如果工具注册表里没有做角色区分客服智能体就具备了超出自身权限的操作能力。等到某个用户让客服帮“根据订单金额把价格改一下”越权操作就顺理成章地发生了。跨租户越权租户 A 访问租户 B 的数据这是最恶劣的越权也是多租户系统必须从架构上杜绝的。多智能体平台往往部署在共享基础设施上工具网关为所有租户服务。如果网关解析 token 后没有把 tenant_id 绑定到工具调用的数据查询中或者业务系统在实现时忘了加租户过滤很容易出现租户 A 的用户拿到了一个租户 B 的订单 ID然后通过共享工具查到了订单明细。2. 多租户权限隔离的三条路线2.1 方案一工具网关统一拦截 上下文透传我首推的方案也是目前绝大多数多智能体平台的主流做法。核心思想是所有工具调用全部收敛到一个统一的网关入口网关负责三件事——解析调用者身份、校验工具级权限、透传身份上下文到后端。用生活化的比喻来说这就像公司大楼的访客系统。任何一个访客想进任何一间办公室都必须先到前台办访客卡。前台会核实你是哪个公司的、去哪一层、找谁然后把访客卡上的信息同步给对应楼层的门禁。办公室里的员工看到访客卡就知道这个人是谁授权进来的不会再问第二遍。这套方案对智能体系统最大的价值在于权限校验不需要依赖智能体“自觉”。智能体的 Prompt 可以被诱导、被注入但它发出的请求只要经过网关网关就会强制要求携带标准身份凭证。即便大模型被用户说服去尝试越权操作网关口也能拦下来。代码上网关层要做到以下几点// 工具网关的核心拦截逻辑Node.js 伪代码 async function handleToolRequest(req, res) { const token req.headers.authorization?.replace(Bearer , ); if (!token) { return res.status(401).json({ error: missing token }); } // 1. 解析身份 const identity await authService.verify(token); if (!identity) { return res.status(401).json({ error: invalid token }); } // 2. 工具级鉴权 const toolName req.body.tool; const toolDef toolRegistry.get(toolName); if (!toolDef) { return res.status(404).json({ error: tool not found }); } const allowed await policyEngine.check({ tenantId: identity.tenantId, userId: identity.userId, roles: identity.roles, action: toolDef.action, resourceType: toolDef.resourceType, }); if (!allowed) { return res.status(403).json({ error: forbidden }); } // 3. 透传身份上下文 req.headers[X-Tenant-Id] identity.tenantId; req.headers[X-User-Id] identity.userId; req.headers[X-User-Roles] identity.roles.join(,); // 4. 转发到工具执行器 return forwardToExecutor(req, toolDef); }这段代码看起来不复杂但关键的细节在于第 3 步。很多系统只做到第 2 步——校验这个人能不能调用这个工具——就放行了。真正的越权往往发生在业务系统内部比如工具实现的 SQL 查询里没有带租户条件。所以在网关层把tenant_id、user_id以标准请求头形式透传下去是业务系统做资源级隔离的前提。2.2 方案二工具实例按租户隔离如果你所在的是金融、医疗这类强合规行业对数据隔离的要求是“物理级”的那建议直接采用方案二。每个租户启动独立的一套工具服务实例每个实例只配置当前租户的存储凭证、密钥和策略进程之间天然隔离。这个方案的隔离效果最好因为从操作系统层面就杜绝了跨租户访问的可能。但它的问题也很突出成本高、运维复杂。每接入一个新租户就要多部署一套服务多维护一组依赖。在多智能体数量快速增长的时候这种模式很难做到弹性伸缩。我个人的判断是除非合规要求强制否则不建议一上来就做全量实例隔离。可以先走方案一的网关透传再配合数据层的租户隔离等业务量到达一定规模后再针对最敏感的工具单独做实例隔离。资源和安全的平衡点需要根据业务实际情况去卡。2.3 方案三数据层资源级 ACL兜底的最后一道防线前两个方案解决的是“请求能不能到这个工具”的问题而资源级 ACL 解决的是“到了工具之后这份数据能不能碰”的问题。方案一、二做得再好如果业务系统里有一条 SQL 查询没有带租户或用户过滤条件工具层校验得再严格也是白搭。资源级 ACL 的核心做法是在每一份资源记录上标记归属信息。我用订单数据来举例-- 错误写法任何人传一个 order_id 就能查到订单 SELECT * FROM orders WHERE order_id $1; -- 正确写法强制拼接租户和用户可见范围 SELECT * FROM orders WHERE order_id $1 AND tenant_id $current_tenant_id AND (user_id $current_user_id OR visibility IN (public, team));这里的关键不是让每个开发者自己记住写条件而是在数据访问层做一个统一的拦截器。比如在 ORM 层面定义全局过滤器或者封装一个SecureRepository所有工具通过它来访问数据它自动从上下文里读取 tenant_id 并拼接到查询语句中。这样就算某个工具的开发者经验不足遗漏了权限过滤条件底层机制也会兜住。3. 实操过程从零搭建一套防越权的共享工具平台3.1 第一步定义统一的工具调用协议与身份上下文很多团队做工具平台时只定义了工具的参数和返回结构却忽略了“身份上下文”。这就是第一版容易越权的主要原因。我在内部定了一套标准每个工具调用都会包装成一个 ToolRequest 对象身份上下文是必填字段{ tool: file.read, params: { path: /documents/123/design.pdf }, context: { tenant_id: t_8fd1, user_id: u_1002, roles: [analyst, team_lead], request_id: req_9f3a28, trace_id: trace_7e52ab } }这里有一个实践中容易被忽略的场景很多工具调用不是用户直接触发的而是智能体自主发起的定时任务或后台任务。比如“每天早上 9 点自动生成销售简报”的定时智能体它没有用户上下文只有系统身份。这种情况下不能把user_id留空而是应该设置为一个专门的system账号并且这个账号只能访问它被明确授权的最低权限数据集合。{ context: { tenant_id: t_8fd1, user_id: system:cron_sales_report, roles: [system_cron], request_id: req_auto_2041, trace_id: trace_cron_001 } }这里的关键原则是不信任智能体自己填写的身份字段。身份信息必须由编排层或者网关统一注入不能被智能体输出的参数覆盖。我见过一些团队把 user_id 放在了工具参数里结果用户用 prompt 注入了别人的 user_id直接绕过了身份体系。3.2 第二步实现带资源级校验的工具网关网关的核心代码在上文已经给出了一个 Node.js 版本。我再用 Python 写一个更接近生产环境的版本大家可以对照。重点看resource_level_check这一步这是方案一和方案三结合的关键。# gateway.py 简化版 import json from fastapi import FastAPI, Request, HTTPException app FastAPI() class ToolContext: def __init__(self, tenant_id: str, user_id: str, roles: list, request_id: str): self.tenant_id tenant_id self.user_id user_id self.roles roles self.request_id request_id def verify_token(token: str) - dict: # 接入你的鉴权服务返回身份信息 identity auth_service.verify(token) if not identity: raise HTTPException(status_code401, detailinvalid token) return identity async def resolve_context(request: Request) - ToolContext: token request.headers.get(Authorization, ).replace(Bearer , ) identity verify_token(token) return ToolContext( tenant_ididentity[tenant_id], user_ididentity[user_id], rolesidentity[roles], request_idrequest.headers.get(X-Request-Id, ), ) def tool_level_check(ctx: ToolContext, tool_def: dict) - bool: allowed_roles tool_def.get(allowed_roles, []) if not allowed_roles: return True # 没有配置角色限制的工具默认放行 return any(role in allowed_roles for role in ctx.roles) def resource_level_check(ctx: ToolContext, tool_name: str, params: dict) - bool: # 每个工具通过 ResourcePolicy 定义资源级校验逻辑 checker resource_policy_registry.get(tool_name) if not checker: return True # 没有资源级策略的工具默认放行 return checker.check(ctx, params) app.post(/tools/{tool_name}) async def invoke_tool(tool_name: str, request: Request): ctx await resolve_context(request) body await request.json() tool_def tool_registry.get(tool_name) if not tool_def: raise HTTPException(status_code404, detailtool not found) # 第一层工具级鉴权 if not tool_level_check(ctx, tool_def): raise HTTPException(status_code403, detailforbidden: tool level) # 第二层资源级鉴权 if not resource_level_check(ctx, tool_name, body.get(params, {})): raise HTTPException(status_code403, detailforbidden: resource level) # 透传身份到业务系统 business_headers { X-Tenant-Id: ctx.tenant_id, X-User-Id: ctx.user_id, X-User-Roles: ,.join(ctx.roles), X-Request-Id: ctx.request_id, } return await tool_executor.execute(tool_name, body.get(params), ctx, business_headers)注意代码里的双层校验工具级鉴权管“能不能用这个工具”资源级鉴权管“能不能碰这份数据”。两层缺一不可。3.3 第三步为每个工具编写资源级权限策略资源级策略我不建议写在业务代码里最好用配置文件或独立的策略规则表达。这样业务同学可以直接维护不用改动代码。我常用的格式是 YAMLtools: order.query: allowed_roles: [admin, analyst, cs_agent] resource: order ownership_required: true tenant_required: true visible_scope: same_tenant order.delete: allowed_roles: [admin] resource: order ownership_required: true tenant_required: true visible_scope: same_owner report.generate: allowed_roles: [admin, analyst] resource: report ownership_required: false tenant_required: true visible_scope: same_tenant策略里的visible_scope控制数据可见范围。same_owner意味着只能操作当前user_id拥有的订单same_tenant意味着只能操作当前租户的数据。策略引擎解析的时候会把tenant_required和ownership_required自动缝合成 SQL 条件传给业务系统的查询层。对于多智能体系统这里还有一个额外的考量不同智能体可能被赋予不同的角色集合。比如采购助手是analyst客服智能体是cs_agent管理员助手是admin。角色在智能体创建的时候就绑定工具调用时通过上下文传入策略引擎根据角色集合放行。3.4 第四步越权测试用例矩阵很多人上线前只测“正常功能能不能用”忘了测“不该用的能不能用”。我在内部固化了一套越权测试矩阵每次工具新增或权限策略调整都要跑一遍。这里分享给大家用例编号测试场景请求身份目标资源预期结果TC-001水平越权租户A用户1租户A用户2的文档拒绝TC-002垂直越权租户A客服角色租户A订单删除操作拒绝TC-003跨租户越权租户A用户租户B订单拒绝TC-004系统身份越权system:cron 角色用户私有文档拒绝TC-005未认证请求无token任意工具401TC-006token过期过期token任意工具401TC-007参数篡改租户A用户将请求参数中tenant_id改为租户B拒绝TC-008角色伪造客服角色伪造admin角色头拒绝网关以token解析结果为准我特别想强调 TC-007 和 TC-008。很多越权漏洞其实不是工具层漏了而是信任了请求参数里用户自己传的tenant_id、user_id、role。记牢一条铁律所有身份信息以网关解析 token 的结果为准任何来自请求体或参数的“身份字段”都视为不可信输入直接忽略。3.5 第五步缓存和数据查询的租户隔离越权不一定只发生在接口层缓存层也会。我在项目里就遇到过一起两个工具共用同一个 Redis key缓存里存了一份某租户的订单列表。结果租户 A 的智能体先查了一次租户 B 的智能体再查同一个 key直接命中缓存返回了租户 A 的数据。从这次事故之后我的缓存 key 设计规矩是任何涉及租户或用户的缓存 key必须带上tenant_id和user_id作为前缀。# 错误的缓存 key cache_key forder:list:{params[page]} # 正确的缓存 key cache_key forder:list:{ctx.tenant_id}:{ctx.user_id}:{params[page]}查询层同样如此。如果工具内部走的是 ORM建议在全局查询过滤器里自动注入tenant_id让每个查询默认带上租户条件除非明确的内部方法申明跳过。4. 常见问题与排查技巧实录4.1 五个高频问题速查表我把这段时间帮同事排查时遇到的高频问题整理成了一个速查表大家可以直接对照排查现象根因解决方案智能体在 Prompt 里被诱导发起越权工具调用Prompt 不稳定无法作为安全边界网关强制鉴权不依赖智能体自律工具参数里的 userId 被用户篡改信任了请求体中的身份字段身份只从 token 解析参数中的身份字段直接丢弃工具层校验通过但业务系统接口被 SQL 绕过业务系统没做二次资源级过滤数据访问层统一拼接 tenant_id 和 ownership 条件缓存命中了别的租户数据缓存 key 没有包含租户维度所有缓存 key 增加 tenant_id/user_id 前缀多智能体编排链路中第二跳工具调用丢了身份编排层没有透传 context使用追踪 ID 关联全链路每跳工具调用强制复制身份上下文其实这些问题大部分都能回归到同一个原则身份信息的解析和透传必须是一条不中断的链路。只要链路断了一环就会产生权限空白。4.2 快速定位越权漏洞的排查思路如果收到越权告警或者安全测试发现了越权漏洞该怎么快速定位我常用的排查路径是这样的。先看请求入口。到工具网关查日志确认这次请求是从哪个智能体、哪个租户、哪个用户发起的。然后看网关有没有正常解析出身份有没有做工具级鉴权。如果网关日志显示身份正常、权限校验通过那问题多半出在业务系统内部。再看业务系统。确认它是不是取用了网关透传的X-Tenant-Id和X-User-Id头。很多业务系统是历史遗留系统自己维护了一套用户体系网关透传的头直接被忽略代码里还是从请求体里读身份这就导致的越权。最后看数据访问层。把工具最终执行的 SQL 捞出来看看有没有带上租户和归属条件。如果一条 SQL 里where条件只有id没有tenant_id那就是典型的资源级越权漏洞。4.3 几个亲身踩过的坑讲几个我踩过得比较深的坑给大家做个反面教材。坑一在 Prompt 里写“不要访问别人的数据”项目刚起步时我们想当然地认为智能体是“智能”的只要在系统 Prompt 里强调安全规则它就会遵守。某次安全演练中测试员用一个精心构造的对话成功诱导智能体调用了本不该访问的文档工具。那一刻我才真正意识到大模型是基于概率生成文本的它不理解“安全边界”的绝对性。从这以后我再也不把任何安全责任放进 Prompt。坑二工具注册表里没有角色维度第一版工具网关的鉴权模型只有“租户”维度没有“角色”维度。导致客服智能体和管理员智能体用同一个工具时都是按相同的权限策略放行。发现这个问题后我在工具定义里强制要求声明allowed_roles没有声明的工具在注册时直接拒绝。坑三调试时为了方便临时给 system 账号开了全部权限有一次为了快速验证一个数据修复流程我临时给system账号配置了所有工具的全部权限修复完成后忘了撤回。结果这个高权限账号在配置里保留了整整两周。期间如果智能体被 prompt 注入就能借 system 身份做任意操作。这里我的教训是高权限系统身份必须走审批流程并且在配置完成后立即确认权限清单不留临时后门。5. 最后补一句权限隔离这件事最怕的不是技术上的复杂度而是“觉得智能体能自己判断”的侥幸心理。智能体是放大器能力越强越权带来的破坏就越大。把权限判断从智能体手里挪到基础设施层所有工具调用统一走网关、统一解析身份、统一做资源级校验这才是一条稳妥的路。在做这套方案的时候我还建议大家把越权测试从“上线前的一次性动作”改成“每次工具变更时的例行检查”。甚至可以在 CI 里加一道越权测试用例任何新增或修改的工具没有通过测试矩阵就不允许合并。这些看起来是繁琐的流程但真等到某一天你的智能体平台接入了上百个工具、几十个租户你会感谢当初多花的那一个下午。
返回列表