众力资讯网

AI 生成 SQL 最危险的不是慢,而是查到了不该看的数据

AI 生成 SQL 最危险的不是慢,而是查到了不该看的数据AI 生成一条慢 SQL,通常没那么难发现。开发环境跑一下,接

AI 生成 SQL 最危险的不是慢,而是查到了不该看的数据

AI 生成一条慢 SQL,通常没那么难发现。

开发环境跑一下,接口响应慢了;测试环境压一轮,数据库 CPU 上来了;上线以后还有慢查询日志、APM 和监控告警。哪怕前面都漏掉,用户也会很快反馈:“这个页面怎么一直转圈?”

越权 SQL 却完全不同。

它可能执行得很快,索引也用上了,返回的数据格式看起来没有任何问题。销售打开客户列表,正常看到了两百条记录,只是其中三十条属于另一个部门;某个租户导出订单时,多带出了其他公司的几行数据;已经软删除的客户重新出现在统计报表里,也可能只是被当成历史数据没有清理干净。

接口没报错,数据库没报警,页面甚至比以前更快。

这类问题可能隐藏几个月,直到客户投诉、内部审计或者员工无意中看到一条不该出现的数据,团队才发现系统的权限边界早就被一条 SQL 绕过去了。

语法正确,不代表业务上可以执行

现在让 AI 写查询代码很方便。把表结构、字段说明和需求发过去,它很快就能生成 Mapper、动态条件、分页查询,顺便把索引建议也补上。

例如一个 CRM 客户查询,AI 很容易写出这样的 SQL:

SELECT
id,
customer_name,
contact_name,
mobile,
update_time
FROM crm_customer
WHERE customer_name LIKE CONCAT('%', #{keyword}, '%')
ORDER BY update_time DESC
LIMIT #{offset}, #{pageSize};

从数据库角度看,这条 SQL 没有明显错误。但放进企业系统里,它至少缺了三层边界:

AND tenant_id = #{currentTenantId}
AND deleted = 0
AND dept_id IN (当前用户可访问的部门)

有些系统还要继续判断数据范围:只能看自己创建的客户、只能看自己负责的客户,还是可以查看本部门及下级部门的数据。

这些条件不会自然出现在表结构里。AI 能看到 tenant_id 字段,却不知道它是不是安全边界;能看到 dept_id,却不知道不同角色对应的是“本人、本部门、本部门及下级部门,还是全部数据”。

在 ERP、CRM、进销存和 OA 里,租户、部门、角色和软删除并不是普通筛选条件。用户可以不传客户名称,可以不选订单状态,但数据权限不能因为参数为空就消失。

多租户条件不能交给前端传

一种很常见的写法,是把租户 ID 当成普通查询参数:

<if test="tenantId != null">
AND tenant_id = #{tenantId}
</if>

看起来很灵活,问题也正出在“灵活”上。

前端漏传 tenantId,查询就可能退化成全表范围;如果接口允许客户端自行提交租户 ID,还要防止用户直接改参数访问其他租户的数据。

租户信息应该来自登录会话、Token 解析结果或服务端可信的安全上下文,再由统一的数据访问层注入。当前请求拿不到租户上下文时,系统应该拒绝查询,而不是把租户条件省略。

部门权限也是一样。下面这种动态 SQL 很危险:

<if test="deptIds != null and deptIds.size() > 0">
AND dept_id IN
<foreach collection="deptIds" item="deptId"
open="(" separator="," close=")">
#{deptId}
</foreach>
</if>

如果一个用户没有任何可访问部门,deptIds 可能是空集合。此时直接跳过条件,结果不是“查不到数据”,反而变成“所有部门都能看到”。

安全条件遇到空值时必须遵循关闭原则:没有权限范围就返回空结果,或者直接拒绝请求,不能自动放宽查询。

动态 SQL 最容易把权限条件悄悄拆掉

AI 很擅长按照需求增加查询条件,但它不一定理解这些条件之间的优先级。

例如业务要求:“可以看本部门数据,也可以看自己创建的数据。”AI 可能生成:

WHERE tenant_id = #{tenantId}
AND dept_id IN (...)
OR creator_id = #{userId}

SQL 中 AND 的优先级高于 OR,这条语句实际表达的是:

(
tenant_id = #{tenantId}
AND dept_id IN (...)
)
OR creator_id = #{userId}

只要 creator_id 命中,后半部分就可能绕开租户限制。正确写法至少应该明确括号:

WHERE tenant_id = #{tenantId}
AND (
dept_id IN (...)
OR creator_id = #{userId}
)

类似问题还经常出现在 <choose>、可选联表、空集合处理、数据范围拼接和 ${} 字符串替换里。

动态 SQL 的风险不只来自注入。即使所有参数都使用了 #{},只要权限条件被错误组合、条件为空时被跳过,仍然可能产生稳定、快速而且很难察觉的越权查询。

这类 AI 写代码后的工程风险,我后面还会继续拆,尤其是 SQL、权限、测试和业务规则怎么在 Java 项目里兜住。关注这些方向的话,可以顺手点个赞,让平台多推荐一些类似内容。

联表查询只限制主表,也不一定够

企业系统里的查询很少只查一张表。

订单列表可能同时关联客户、联系人、商品、价格、成本和收款记录。很多代码只在主表上增加租户条件:

SELECT
o.order_no,
c.customer_name,
p.product_name,
d.cost_price
FROM sales_order o
LEFT JOIN crm_customer c ON c.id = o.customer_id
LEFT JOIN product p ON p.id = o.product_id
LEFT JOIN product_cost d ON d.product_id = p.id
WHERE o.tenant_id = #{tenantId};

如果系统使用全局唯一主键,数据库又严格保证不同表之间的租户关系,这样写未必立刻出问题。

现实项目往往没有这么理想。很多企业数据库没有外键约束,还存在历史数据导入、人工修复、租户迁移和旧版本遗留数据。一旦关联关系中混入了错误记录,主表租户条件并不能阻止查询把其他租户的客户名称、联系方式或成本价格带出来。

联表查询上线前,至少要确认三个问题:关联表是不是同样受租户约束,关联关系是否在数据库层得到保证,以及成本、手机号、回款等敏感字段是否还需要额外的字段级权限。

子查询、UNION、临时表和统计 SQL 也要逐段检查。权限条件加在最外层,并不代表内部每一个数据来源都安全。

列表有权限,导出和报表却经常漏掉

很多系统的列表接口会经过统一权限拦截器,导出、定时任务和报表查询却重新写了一套 SQL。

页面上只能看本部门客户,点击“导出全部”以后,后台任务直接调用另一个 Mapper;销售订单列表隐藏了成本字段,经营报表为了计算毛利,又把成本数据一起查了出来;详情接口做了权限判断,count、exists 和批量查询却没有使用同样的数据范围。

越权问题经常藏在这些不常走的入口里:

Excel 导出和批量打印;首页统计、经营大屏和自定义报表;定时任务、消息推送和数据同步;根据 ID 批量查询详情;count、sum、exists 等聚合查询;管理后台临时增加的原生 SQL;异步线程没有正确传递租户上下文。

权限设计不能只覆盖“用户最常打开的列表页”。只要代码能够读取数据,就应该处在同一套数据边界之内。

为什么 AI Code Review 容易漏掉越权 SQL

AI 做代码审查时,通常更容易发现这些问题:有没有 SQL 注入、是否缺少索引、分页参数是否合理、是否可能出现全表扫描、Mapper 参数能不能正确绑定。

越权查询更依赖项目上下文。

它需要知道当前用户的数据范围规则,理解租户 ID 从哪里取得,识别哪些表启用了软删除,还要知道某个 @DataScope 注解、MyBatis 拦截器或自定义权限组件究竟覆盖了哪些查询。

如果权限规则只存在于开发人员的经验里,AI 很难凭一段 SQL 推断出来。

测试同样容易制造安全感。AI 生成的用例通常验证“能不能查到数据”,却很少主动验证“不能查到什么数据”。测试库里如果只有一个租户、一个部门和一个管理员账号,所有越权 SQL 都可能顺利通过。

因此,权限测试必须包含反向断言:

assertThat(result)
.extracting(CustomerVO::getTenantId)
.containsOnly(currentTenantId);

assertThat(result)
.noneMatch(customer ->
forbiddenDeptIds.contains(customer.getDeptId()));

更可靠的测试数据,应该故意创建名称相同、编号相近但属于不同租户和部门的记录。只有这样,遗漏权限条件时测试才会立刻失败。

Java 项目里,权限条件应该尽量集中管理

让每个开发者在每条 SQL 里手动补 tenant_id、dept_id 和 deleted,长期一定会漏。AI 加入以后,Mapper 和报表 SQL 生成得更快,遗漏概率反而可能进一步放大。

租户边界适合由统一的数据访问层、ORM 拦截器或数据库行级安全能力处理;部门数据范围可以通过统一的权限服务生成,再由 SQL AST 拦截器或受控查询构造器注入。

但拦截器也不是装上就结束了。原生 SQL、复杂子查询、表别名、插件执行顺序、异步任务和绕开 ORM 的数据访问方式,都可能让自动注入失效。

更稳妥的原则是:

安全条件与普通业务筛选分开管理;租户和用户身份只从可信上下文获取;缺少安全上下文时默认拒绝;所有绕开统一拦截器的 SQL 必须显式标记并人工审查;数据权限测试必须覆盖“允许访问”和“禁止访问”两种结果。SQL 上线前,可以按这张表过一遍

检查区域

上线前需要确认

租户隔离

tenant_id 是否来自服务端可信上下文;上下文缺失时是否拒绝查询;子查询、联表和 UNION 是否同样受约束

部门权限

是否正确处理本人、本部门、下级部门和全部数据;空权限集合会返回空结果,还是错误地放宽范围

软删除

主表和关联表是否都过滤了删除数据;统计、导出和历史查询是否有明确规则

动态 SQL

权限条件是否被 <if> 变成可选项;AND、OR 是否使用了正确括号;空集合和空参数如何处理

联表查询

关联表是否可能跨租户;敏感字段是否需要角色或字段级权限;历史脏数据会不会破坏租户关系

查询入口

列表、详情、导出、报表、定时任务、批量查询、count 和 exists 是否使用同一权限规则

框架拦截

原生 SQL、异步线程、表别名和复杂子查询是否绕过租户或数据权限插件

自动化测试

是否准备两个租户、多个部门、已删除数据和空权限账号;是否包含“绝对不能查到”的反向断言

审计记录

是否记录操作者、租户、查询入口、导出行为和返回数据量;异常的大批量查询能否被发现

人工复核

业务负责人能否说清楚“谁可以看哪些数据”;Code Review 是否有人专门按权限模型检查

这张表不只适用于 AI 生成的 SQL。人工写的查询一样会漏权限,只是 AI 把代码生成速度提高以后,一周内新增的 Mapper、报表和导出接口可能比以前多几倍,原来靠经验和人工记忆维持的安全边界会更快暴露问题。

AI 可以帮我们写 SQL,却不会自动替企业定义数据责任。

慢查询影响的是体验和资源,越权查询碰到的却是客户隐私、商业数据和企业信任。以后审查 AI 生成的 SQL,别只看它会不会全表扫描,也要确认这条语句到底代表谁在查询,以及它绝对不能看到哪些数据。

有做多租户、CRM、ERP 或数据权限项目的朋友,也欢迎补充你遇到过的情况。后面我会继续聊 AI 写 Java 代码以后,真实项目里该怎么把权限、测试和业务规则兜住。