PROD欧洲主权BaaS平台打开仪表板 →

原生人工智能 · 9 最小读取值

保护 NL2SQL 免受 LLM SQL 注入

Affane Daylami · Fondateur · 2026年4月16日

返回博客

NL2SQL 端点将自然语言问题转换为 SQL 查询,然后针对您的数据库执行该查询。因此,风险不是经典的 SQL 注入,即某种形式的严重转义字符串:它是一种语言模型,它单独决定要编写哪个 SQL。礼貌地要求模型仅在系统提示符中生成 SELECT 不会在结构上阻止任何操作,它是一条指令,而不是访问控制。唯一有效的方法是使用构造其语法树的解析器来验证随后生成的 SQL。

该英文文本是根据法文原文自动生成的,尚未经过审查。
该页面已自动翻译。英文版具有权威性。

This guide details the method that actually works: structural restriction to SELECT, closed whitelist of functions, mandatory row cap, and locking of the queried schema. Each step relies on the validator actually implemented in Aurabase's NL2SQL engine, a capability of its native AI built into thebackend, not a third-party service thrown together after the fact. If the subject is new to you, our overview of NL2SQL lays the foundations, and the step-by-step tutorial shows how to build the complete endpoint.

要点

  • 提示工程(“仅生成 SELECT”)是 而不是安全检查:模型可能会产生幻觉,被模糊的问题引导,或者干脆忽略指令。
  • 所持有的验证是结构化的:解析器构建请求的语法树(AST),并默认拒绝任何未明确授权的内容。
  • 四个具体层限制了风险:严格的 SELECT(既不是子查询,也不是 CTE,也不是 UNION)、十个函数的封闭白名单、LIMIT 强制和上限、阻止对系统目录和非租户模式的访问。
  • 查询的模式必须来自 服务器,而不是来自客户端请求中的字段:否则没有什么可以阻止调用者提供自己的模式来绕过验证。
  • 在 Aurabase,此验证器(Rust 箱 sqlparser)使用代码中记录的对抗性案例进行测试:隐藏在 FILTER、内部聚合 ORDER BY或 OFFSET中的禁止函数。
#
真正的问题

为什么系统提示中的指令不会阻止任何内容?

系统提示“仅生成 SELECT 查询”是一种偏好,而不是障碍。该模型在大多数情况下都会尊重它,因为它已经接受过遵循指令的训练,而不是因为技术限制在物理上阻止它编写任何其他内容。两类故障使生产信心不足。

第一个来自问题本身。出于恶意或只是在表述上有创意的用户可以以这样的方式引导问题,将模型推向他们不应该编写的 SQL:连接到敏感表、绕过预期逻辑的过滤器、系统函数调用。该模型不区分合理的问题和旨在操纵该问题的问题。

第二个不需要恶意。模型可以幻觉一个表名,忘记提示符要求的 LIMIT,或者生成一个 SELECT *,而对大表没有任何限制。这两种情况的结果是相同的:潜在昂贵或侵入性的 SQL,它已通过提示过滤器,并将针对真实数据库执行。

有帮助的图像

系统提示仍然有用,它在大多数情况下引导模型走向正确的结果。但“禁止进入”的标志并不能阻止任何不知道如何阅读或决定忽略它的人。你需要后面有一扇关上的门,而不仅仅是前面的面板。

#
步骤1

将生成的 SQL 解析为语法树,而不是原始字符串

第一道防线是使用目标方言的真实解析器来解析模型生成的 SQL,然后验证结果结构,而不是原始文本。对字符串中禁止单词(“DROP”、“DELETE”、“;”)的搜索被轻松绕过:不同的大小写、在关键字中间插入注释、键入引号。语法树明确地描述了查询实际执行的操作。

Aurabase 使用 Rust 箱 sqlparser 及其方言 PostgreSqlDialect实现此步骤。即使在解析之前,第一个词法过滤器也会拒绝树中一旦出现就很难正确推理的两种结构:美元引用($$...$$),它可以隐藏字符串中的任意内容,以及多行注释(/* */),它可以隐藏指令的真正结尾。

nl2sql/validator.rsrust
let dialect = PostgreSqlDialect {};
let mut statements = Parser::parse_sql(&dialect, sql)
    .map_err(|e| AuraError::BadRequest(format!("Erreur de parsing SQL: {}", e)))?;

if statements.len() > 1 {
    return Err(AuraError::BadRequest("Multi-statements non autorisés".to_string()));
}

这种对多语句的拒绝仅通过堆栈阻止了最著名的 SQL 注入形式: SELECT * FROM users; DROP TABLE users;--。解析器仅返回一个可操作的指令,第二个指令根本就不会到达,无论原始问题中的措辞如何。

#
步骤2

结构上限制为简单的 SELECT

获得树后,最广泛的验证是仅接受一种类型的根节点,即查询 (Statement::Query),并拒绝其他所有内容: INSERT、 UPDATE、 DELETE、 DROP、 CREATE、 ALTER。它不再是提示指令,而是解析对象类型的条件,任何熟练的问题表述都无法规避这一条件。

即使在 SELECT 中,一些构造仍然是危险的,值得明确拒绝:

施工被拒绝为什么它很危险?
CTE / 与可以在最终 SELECT 之前链接额外的非预期逻辑。
子查询、UNION / INTERSECT / EXCEPT扩展了单个问题在单个查询中可以执行的操作的范围。
选择...进入创建一个表:伪装成读取的写入。
更新/分享安装锁,与生产流量争用的风险。
表函数(generate_series、pg_read_file...)通过按需生成的线路进行系统访问或拒绝服务。

从存储库中获取的测试用例具体说明了最后一点:SELECT * INTO backup FROM users 被拒绝,即使它既不包含可见的写入关键字也不包含可疑函数。请求的形式足以取消其资格。

#
步骤3

功能白名单,不黑名单

禁止函数的黑名单(pg_sleep、 pg_read_file、 dblink...)需要一一预测每个危险函数,而 Postgres 会暴露数百个危险函数。白名单颠倒了举证责任:仅授权十个函数, count、 sum、 avg、 min、 max、 lower、 upper、 coalesce、 date_trunc、 now。默认情况下,其他一切都是不允许的,包括一项尚未有人考虑添加的合法功能。

一次验证通过并不总是足够的。树的结构遍历一一列出了它的入口点(投影、WHERE、JOIN、GROUP BY...),并且很容易忘记一个:禁止的函数可以隐藏在 FILTER (WHERE pg_sleep(10) IS NOT NULL)子句中、内部聚合 ORDER BY (sum(id ORDER BY pg_sleep(10))) 中、 WITHIN GROUP、 DISTINCT ON或 OFFSET中。

nl2sql/tests.rsrust
#[test]
fn test_rejette_fonction_interdite_dans_filter() {
    assert!(validate(
        "SELECT count(*) FILTER (WHERE pg_sleep(10) IS NOT NULL) FROM users",
        None, None
    ).is_err());
}

因此,Aurabase 验证器添加了第二个详尽的传递,它独立于结构路径,遍历树中的所有表达式,无论它们位于何处。这是一种假设的纵深防御:如果第一次错过某个案例,第二次就会赶上。

#
步骤4

限制返回的行:LIMIT 强制且有上限

SELECT * 保持授权,对于数据挖掘很有用。风险不是明星,而是模型编写的查询缺乏上限:一个表述不当的问题可能会带回整个表,以及这意味着内存成本和响应时间。

Aurabase 采用简单且透明的规则。如果生成的 SQL 没有 LIMIT,服务器会添加一个(默认 100 行,在系统提示符中向模型公布该值)。如果 SQL 请求的 LIMIT 超出硬上限(默认情况下为 1000 行),则查询将被显式拒绝,而不是默默地减少。这两个值都可以在服务器端配置(AI_NL2SQL_DEFAULT_LIMIT、AI_NL2SQL_MAX_LIMIT),如果故障超过上限,服务器甚至会拒绝启动。

réponse /v1/ai/{project_id}/nl2sql (extrait)json
{
  "sql": "SELECT count(*) FROM orders WHERE status = 'paid' LIMIT 100",
  "limit": 100,
  "limit_injected": true
}
信息

拒绝而不是默默地削减有直接的利益:在没有明确说明的情况下应用上限会给呼叫者一种他的请求得到尊重的错觉,而结果会在他不知情的情况下被截断。 limit_injected 始终表示该值来自模型还是服务器。

#
步骤5

锁定对架构的访问:系统目录和跨架构

两个不同的泄漏威胁着连接到真实数据库的 NL2SQL 引擎:对 Postgres 系统目录的访问,以及对不属于调用者的模式的访问。无论下游放置任何 RLS 策略如何,两者都会在验证时阻塞。

pg_catalog 始终是 search_path的一部分,这意味着像 pg_authid 或 pg_stat_activity 这样的非限定名称可以直接访问它,无需前缀。 Aurabase 验证器会阻止任何以 pg_开头的名称,以及 information_schema 和内部架构 aura_console,无论是否合格。

在两部分名称 (schema.table) 上,仅允许调用项目的架构,拒绝任何其他值。包含三个或更多组成部分的名称将被自动拒绝。生成的查询级别的此边界是对我们关于多租户隔离的文章中详细介绍的数据库级别隔离的补充:一个可以防止生成的 SQL 定位另一个架构,另一个可以防止连接本身到达另一个数据库。两者都不能取代另一个。

#
步骤6

永远不要让客户端重新定义查询的模式

离散陷阱等待任何接受描述客户端查询中允许的架构或表的参数的 NL2SQL API。如果使用相同的参数来构造提示并验证输出 SQL,则调用者可以对允许的内容撒谎,然后验证将根据此谎言进行验证,而不是根据数据库的实际情况进行验证。

Aurabase 会在每次调用时内省实际的项目基础架构,并使用短短的 30 秒缓存来提高性能,并显式拒绝(400 错误)请求正文中发送的任何 schema、 allowed_schema或 schema_context 字段,而不是接受然后默默地覆盖它。区别很重要:接受然后忽略的领域会给人一种不存在的控制幻觉;被拒绝的字段会立即显示出来。

#
清单

在生产前审核您自己的 NL2SQL 管道

无论您使用 Aurabase 还是在通用法学硕士的基础上构建自己的流程,以下几点涵盖了最常被忽略的内容。

如果你自己编写验证器

  • 使用适合您的确切方言的真正解析器来解析 SQL,而不是使用字符串中的模式匹配。
  • 采用默认拒绝:任何类型的节点、任何未明确授权的功能都必须被拒绝,而不仅仅是已经识别出的危险情况。
  • 每个查询仅接受一个语句,这是针对堆叠查询的最简单的拒绝。
  • 使用真实的对抗性案例(FILTER、内部聚合 ORDER BY、OFFSET 中禁止的功能)测试验证器,而不仅仅是使用明显的案例。
  • 不管怎样,使用 Postgres 角色执行经过验证的 SQL,并在预期模式上降低权限:验证器限制查询的形式,角色限制它在某个案例逃脱时可以物理实现的目标。

如果您正在评估第三方 NL2SQL 框架

  • 明确询问验证是结构性的(AST)还是只是提示指令:答案会改变一切。
  • 检查默认情况下是否应用线路上限,而不仅仅是记录为最佳实践(费用由您承担)。
  • 检查 API 客户端是否可以提供用于验证的架构,这将重新修复上述缺陷。
  • 选择之前,请根据此特定标准比较多个工具:我们的 NL2SQL 工具比较 详细介绍了 2026 年可用方法的区别。
#
纵深防御

验证器降低了风险,但它不会取代 RLS

可靠的 AST 验证器可以从源头降低风险:到达数据库的 SQL 已经具有已知且有界的形式。但是,它不会取代敏感表上的 RLS 策略,这些策略决定给定用户有权查看哪些行。这两层回答不同的问题:验证器限制生成的查询的形式,RLS 限制它可以为特定用户返回的数据。保持两者都活跃,即使其中一个看起来与另一个多余。

NL2SQL covers structured questions about your tables. For questions about unstructured content, documents, notes, tickets, Aurabase's native RAG follows a comparable security logic, detailed in our RAG pipeline tutorial on pgvector.

#
常见问题解答

常见问题解答

即时工程对于保护 NL2SQL 完全没有用吗?+
不,大多数情况下,它对于将模型引导至正确且相关的 SQL 仍然很有用。但这不是安全检查:模棱两可或被操纵的问题可以绕过它,并且提示指令不会在物理上阻止任何内容。无论提示的质量如何,生成后的结构验证器仍然是必要的。
如果生成的 SQL 已经验证并限制为 SELECT,我们是否还应该激活 RLS?+
是的。验证器限制请求的形式(无子查询、无非白名单函数、上限LIMIT),而不是对数据的业务权限。 RLS 仍然是决定特定用户有权查看哪些行的层,即使在完全有效的 SELECT 中也是如此。
十个功能的封闭白名单是否限制了可能出现的问题太多?+
是的,而且是自愿的。它涵盖了大部分读取分析(count、sum、avg、min、max、lower、upper、coalesce、date_trunc、now),并默认拒绝其他所有内容,包括尚未有人考虑添加的合法函数。每次添加都必须是明确的决定,而不是黑名单中的疏忽。

准备好部署了吗?

五分钟内完成您的后端。

无需信用卡 · 500 MB 免费 · 50,000 MAU