【NL2SQL 实战 06】只读断言:SELECT 之外一律拒绝,凭什么要做三遍

上一篇讲 sqlglot 方言踩坑。评论区有人问:你闸门那么多层,最基本的「只允许 SELECT」到底怎么判,一层不够吗?

不够。 一层判断能挡住 90% 的脏 SQL,剩下 10% 恰恰是出事的那些。这篇讲清楚:为什么这是个真问题、为什么做三遍、三层各自抓什么。

一、先看为什么「只允许 SELECT」是个真问题

模型确实大部分时候会吐 SELECT。但「大部分时候」不是安全边界。

用户说「把上个月的坏数据清掉」,模型可能真的写出 DELETE FROM;用户说「帮我改成已完成」,模型可能真的写出 UPDATE ... SET status = 'done'。它不是恶意的——它在「帮忙」。NL2SQL 接到业务库,「帮忙」写库就是事故。

更要命的是:你没法用 Prompt 拦住它。Prompt 说「不要写库」,模型这次听了,下次换个说法、换温度、换版本就不一定了。意图靠 Prompt 引导,但安全不能靠意图。

所以结论是:「只允许 SELECT」必须做成运行时强制,而不是模型自觉。 这就是 fail-closed 的第一层含义:不确定,就拒绝。

二、为什么做三遍,而不是一遍

脏 SQL 不总是明显的:

  • WITH x AS (INSERT INTO t VALUES (1)) SELECT 1——外层像查询,里面在写库;
  • 某条 SQL 不含任何写库关键字、但结构根本不是查询(方言变体、存储过程调用);
  • 校验通过了,但执行时走了一条绕过校验的「快捷路径」。

每一层挡的是不同类型的漏。 单层再强,也只挡住一种失败模式。三层是不同层级的纵深,不是重复劳动。

遍数在哪怎么判抓什么
第一遍SQL 策略层正则 + 关键字明显含 DML / DDL / 权限语句 / 多语句
第二遍SQL 闸门sqlglot AST最外层不是 SELECT(含 WITH...SELECT
第三遍执行器入口再查一遍校验和执行之间被人加了「捷径」

三、三层各自怎么判

第一遍:正则——挡「明显含写库关键字」。

它是业务库的门卫,不懂语法树,只认关键字,五道关:空 SQL 拒绝;多语句(含分号)拒绝——SELECT 1; DROP TABLE t 是注入经典姿势;DML 关键字拒绝(INSERT / UPDATE / DELETE / REPLACE / MERGE / CALL);DDL 关键字拒绝(CREATE / ALTER / DROP / RENAME / TRUNCATE);权限语句拒绝(GRANT / REVOKE / SET GLOBAL)。过了这五道,再看文本是不是以 SELECTWITH 开头。

正则用单词边界卡关键字,不会误杀表名里的 update(如 update_log)。但它会拦住 CTE 夹带:WITH x AS (INSERT INTO t VALUES (1)) SELECT 1 里的 INSERT 被正则命中——这条 SQL 连 AST 那层都走不到。正则先手,AST 补刀。

第二遍:AST——挡「不含关键字但结构不是查询」。

正则过了,不代表安全。假设有人构造了一条不含任何 DML/DDL 关键字、但结构不是查询的语句——理论上存在这种可能。

sqlglot 解析之后,检查最外层节点:只认 SELECT(或 WITH 包着 SELECT),其他任何 AST 类型——INSERTUPDATECommand——都不过。

两层配合的逻辑:正则挡「明显含写库关键字的」,AST 挡「不含关键字但结构不是查询的」。正则快,AST 准。

第三遍:执行器——不信任任何中间层。

执行器入口第一行就是再查一遍只读。这条 SQL 理论上已经过了前面两层,为什么还要查?

一是纵深:校验节点和执行节点之间有一条图边,今天边上只有权限注入,明天有人加了缓存、加了日志、加了什么「优化路径」——谁保证中间不会把 SQL 换了?执行器不管你从哪来,进门就查。

二是行数兜底:执行器取数据时会多取一行检测溢出——如果数据库返回超过上限的行数,说明 LIMIT 没生效(B4 讲过方言坑的可能),报错而不是默默截断。

四、双库策略:业务库和治理库的规矩不一样

问数系统自己也有一套库(治理库),存用户、元数据、审计日志,这套库需要 INSERT / UPDATE。但它也有规矩:

允许禁止
业务库SELECTINSERT / UPDATE / DELETE / DDL / GRANT / 多语句
治理库SELECT / INSERT / UPDATE物理 DELETE / DDL / 多语句

治理库禁止物理 DELETE——删除用逻辑删除(deleted=1);禁止 DDL——建表改表只能通过版本化 SQL 文件人工执行,不允许代码在运行时改结构。

这不是防模型——治理库不接受模型生成的 SQL。这是防应用代码: 哪天有人写了个 DELETE FROM copilot_sys_user WHERE id = ?,策略层直接拒。比 Code Review 靠谱,因为策略层是运行时强制。

五、「它只是个助手」的错觉

我见过不少 Chat-to-SQL 项目的 README 写着「AI 助手只生成查询,不会修改数据」。这句话描述的是意图,不是机制。Prompt 不是合同。

NL2SQL 接到业务库上,安全论据不是「模型很乖」,而是:即使模型写了 DELETE,正则拒绝;即使正则漏了,AST 判断最外层不是 SELECT,拒绝;即使 AST 也漏了,执行器再查一遍。三道门都不看模型的意图,只看 SQL 字符串的结构。结构不是 SELECT,就不执行。

这就是 fail-closed:不确定就拒绝。 默认行为是拒绝,需要主动满足条件才放行——和 fail-open(默认放行,出了事再补丁)是两种世界观。

六、一张图收尾

解析失败

最外层非 SELECT

通过

模型产出的 SQL

正则: DML/DDL/多语句?

拒绝

正则: 以 SELECT/WITH 开头?

拒绝

sqlglot parse

拒绝

拒绝

表白名单 · 列 · LIMIT · 行级范围

执行器入口: 再查一遍只读

业务库 · 只读

正则在前,AST 在后,执行器兜底。三层都只认 SELECT。

带走三句

  • 「只允许 SELECT」做三遍不是强迫症——三遍拦的位置不同,抓的失败不同
  • 业务库和治理库规矩不同,但都有运行时强制策略,不靠 Code Review 兜底
  • 「它只是个助手」描述的是意图,不是机制。NL2SQL 上生产,机制要 fail-closed

开源仓库在下面,本地 Compose + Fixture 可以直接跑通,不需要先配云 API Key。说明看 docs/DEMO.md / AGENTS.md

GitHub: https://github.com/yanqiuping110-cloud/xb-data-copilot-bot

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值