让AI查业务库,安全边界要跑四次交集

请求清洗、服务端授权、Schema检索和SQL复核,是四个不能互相替代的边界。
摘要| 我沿 Microi吾码AI 当前 NL2SQL 链路,从 V8 请求绑定追到角色策略、FormEngine 权限、Schema 检索与 SQL 校验。结论是:提示词不能授予数据权限;边界来自服务端白名单取交集、空集合失败关闭,以及生成后逐表复核。本文绑定当前源码、行号与 55/55 聚焦测试,不把本地测试冒充生产验收。
✦ ① 提示词不是权限系统
让大模型“只查询有权限的表”听起来合理,却有一个根本问题:模型看到的文字本身不能证明调用者是谁,也不能证明这个人此刻拥有哪些菜单、表和数据范围。如果客户端还能直接传 `AllowedTables`,攻击者只要把数组改成敏感表名,提示词就成了装饰。
Microi吾码AI 当前链路把问题拆成四层:先清掉客户端自报的可信字段,再由服务端计算候选表;Schema 检索只在候选表里找相关项;模型生成 SQL 后,还要逐个检查来源表、语句类型和结果上限。任何一层失败,都不继续向数据库放行。

客户端能表达问题,但不能替服务端声明租户、用户、白名单和行数上限。
先给结论|| 安全的自然语言查询,不是“模型答应不越权”,而是模型从未拿到越权 Schema,生成结果也无法绕过服务端复核。
✦ ② 第一层:把客户端的“可信字段”清零
在 V8 入口,当前代码从认证上下文写入 `OsClient`、当前用户 Id 和名称,同时把调用方传来的 `AllowedTables` 置空,把 `ServerAuthorizationApplied` 改为 false,并把 `ServerMaxRows` 清零。这一步的价值不是格式化参数,而是明确谁有资格产生可信状态。
param.OsClient = _osClient;
param.CurrentUserId = _currentUser["Id"]?.ToString();
param.AllowedTables = null;
param.ServerAuthorizationApplied = false;
param.ServerMaxRows = 0;
信任方向|| 问题文本从客户端进入;租户、用户、授权标记、表集合和行数上限只能由服务端向下游注入。
✦ ③ 第二层:候选表要经过三次取交集
服务端先拿当前租户的普通业务表集合,受保护的控制面表不会因为用户等级高就自动暴露。普通角色还要把 AI 角色策略给出的表、调用者希望缩小的范围,以及 FormEngine 的真实读取权限依次求交集。客户端请求只能缩小,不能扩大。
- 先限定当前租户的普通业务表,排除受保护的控制面表。
- 再与 AI 角色策略、调用方主动缩小的范围求交集。
- 最后叠加 FormEngine 的真实读取权限,得到服务端白名单。
租户普通业务表
∩ AI角色策略表
∩ 调用者请求的缩小范围(可选)
∩ FormEngine真实读取权限
= 服务端AllowedTables
当前实现还为权限结果使用带授权版本号的十分钟缓存。缓存 Key 同时包含租户、授权版本、用户和候选表签名;权限版本变化后不会继续命中旧快照。缓存读取失败时转为实时校验,而不是默认放行。
空集合语义|| 交集结果为零时直接拒绝查询。零不是“让模型自己猜”,也不是退回全租户 Schema。
✦ ④ 有行级范围的表,为什么宁可不查
表级可读不等于可以看整张表。某个菜单可能带 `SqlWhere`,只允许看本人、部门或关联记录;通用 NL2SQL 如果只拿到表名却没有稳定复现这些条件,就会把局部权限误当成全表权限。
当前批量授权只接受两类表:角色有明确的表级 Read 权限,或者至少有一个已授权且没有 `SqlWhere` 的菜单。带行级范围的菜单不会进入通用查询白名单。需要本人、部门或复杂关联范围时,正确做法是写参数化、经过管理员审核并有审计的业务 ApiEngine。
失败关闭|| 当通用 SQL 无法证明能重现数据范围时,拒绝比“尽力加一个条件”更诚实,也更安全。
✦ ⑤ 第三层:检索相关Schema,也必须在授权之后

检索命中回答“哪些表相关”,授权回答“哪些表可见”,两者不能调换。
关键词检索始终可用;只有显式启用向量数据库时,才增加 Qdrant/Ollama 通道。但无论是关键词结果还是向量结果,当前代码都会带入同一份服务端 `AllowedTables`。因此向量召回再聪明,也只能在被授权的表集合里排序。
返回给界面的 `SchemaCandidateCount` 也是授权过滤后的候选数量。它可以解释“本次命中了几张相关表”,却不会把未授权表名或字段当成诊断信息泄露出去。
概念分离|| 相关性检索不是授权;向量相似度不是权限;候选数量也只能统计授权后的集合。
✦ ⑥ 第四层:模型写完SQL,还要逐表验一次
执行层首先要求服务端授权标记为真且白名单非空,然后归一化表名和最大行数。SQL 词法检查会读取每个 `FROM` 与 `JOIN` 来源;只要子查询、显式连接或任一来源出现未授权表,整条语句就失败。没有引用任何授权表的 `SELECT 1` 也不会放行。
生成SQL
→ 只允许受控只读SELECT
→ FROM / JOIN / 子查询逐表匹配
→ 禁止多语句、危险函数和逗号连接
→ 按数据库方言追加最大行数 + 1
→ 只返回授权上限内的结果
多取一行用于判断是否截断,最后只返回授权上限内的数据。这里也要讲清边界:当前是保守词法门禁,不应宣传成完整 SQL AST;模型生成的动态值也不会自动改写为数据库参数。高风险条件仍应回到显式参数化接口。
✦ ⑦ 这次真实跑了55项聚焦测试

当前本地 no-build 聚焦运行:55 通过、0 失败、0 跳过。
我在当前 Microi.Server 运行 `NL2SqlSecurityPolicyTests`:55 项、55 项通过、0 项失败、0 项跳过;VSTest 报告 99 ms,命令墙钟约 2.225 秒。用例覆盖双 JSON 序列化伪造可信字段、空白名单、越权 JOIN 与子查询、向量结果精确过滤、多数据库行数上限、危险函数、多语句和只读语句。
这组测试证明的是当前本地授权上下文与 SQL 安全策略行为;它没有拿生产账号查询真实业务数据,也没有证明目标租户的角色策略已经配置完整。生产验收仍需登录真实角色,核对候选数量、查询结果和审计记录。
验证边界|| 源码、单元测试和生产权限配置是三件事;本文只确认前两件。
✦ ⑧ 结尾:把权限做成可计算的集合
安全的 AI 数据助手,不需要假装模型永不犯错。更可靠的设计,是把租户表、角色策略、真实表权限、无行级范围条件、Schema 候选和 SQL 来源都变成可计算、可记录、可失败关闭的集合。
因此,评审一个自然语言查询能力时,可以连续问四句:谁清除了客户端伪造字段?白名单由哪些权威数据求交集?未授权 Schema 会不会进入检索?生成 SQL 的每个来源表是否再次验证?四句都能落到代码和测试,提示词才只是体验层,而不是被迫承担权限系统。
- 可信租户、用户和白名单是否只由服务端写入?
- 零张授权表时是否立即失败关闭?
- Schema 检索与 SQL 来源表是否都使用同一份授权集合?
- 源码证据、聚焦测试与生产角色验收是否分别陈述?
本文验证范围|| 基于 2026-08-23 当前本地源码 SHA、行号与 55/55 聚焦测试;未执行生产查询、未修改平台数据、未声称线上角色策略已验收。
更多推荐




所有评论(0)