让AI小助手用自然语言查数据库,听着很爽,用户问"上个月华东区销售额多少"它直接给你拉数。但只要你让模型去碰SQL,就得先把安全这关守住,不然出事很快。我搭这玩意儿的时候踩了几个坑,记下来。
最大的威胁是什么
模型生成SQL有两个风险。一个是它可能生成 DROP TABLE、DELETE、UPDATE 这种破坏性语句——你以为它只会查,结果用户一句"把测试数据清掉"它真给你写了删表。另一个是经典SQL注入,用户在自然语言里夹带 ‘; DROP TABLE users; – 这种东西,要是你直接把模型输出拼进执行,就完蛋。
我搭这个智能体用的是一个拖拽配节点的低代码平台,数据库查询那步是它的一个工具节点。但工具节点本身不会替你做安全,得自己加防护层。
第一道:数据库账号只给只读权限
这条最重要,也最省心。我专门建了个数据库账号,只授 SELECT,别的权限一概不给。
CREATE USER ‘agent_ro’@’%’ IDENTIFIED BY ‘你的强密码’;
GRANT SELECT ON sales_db.* TO ‘agent_ro’@‘%’;
– 不给 INSERT/UPDATE/DELETE/DROP/ALTER
FLUSH PRIVILEGES;
智能体连库就用这个账号。这样哪怕模型脑子一抽生成了删表语句,数据库层面直接拒了,权限不够执行不了。这是底线,比任何提示词防护都靠谱。我个人觉得这一条做好了,安全这事就稳了一大半。
第二道:别让模型直接写完整SQL
我没让模型自由生成整条SQL,那太放飞了。改成让模型只输出查询意图的结构化参数,比如查哪张表、哪些字段、过滤条件是什么、时间范围多少,输出成JSON。然后我用一段固定的代码,拿这些参数去拼参数化查询。
模型只输出这种结构,不碰原始SQL
{
“table”: “sales”,
“region”: “华东”,
“start”: “2026-05-01”,
“end”: “2026-05-31”
}
拿到之后用参数化方式执行:
cursor.execute(
“SELECT SUM(amount) FROM sales WHERE region=%s AND dt BETWEEN %s AND %s”,
(params[“region”], params[“start”], params[“end”])
)
注意是 %s 占位符加参数元组,不是字符串拼接。这是防注入的标准做法,用户夹带的恶意内容会被当成普通字符串值,不会被当SQL执行。
这套的代价是灵活性差了点。模型不能想查啥查啥,只能查我预先支持的几种模式。复杂的多表JOIN它干不了。但对大多数业务问答够用,安全换灵活,我认了。
第三道:表名字段名白名单校验
模型输出的 table 字段我不信,万一它编了个表名呢。所以拿到参数后先过一道白名单:
ALLOWED_TABLES = {“sales”, “orders”, “products”}
if params[“table”] not in ALLOWED_TABLES:
raise ValueError(“不允许的表”)
字段名同理。表名字段名这种东西没法参数化(占位符只能填值不能填标识符),只能靠白名单挡。这点容易漏,我一开始就没校验表名,被同事用一句精心构造的话术诱导模型查了张本不该暴露的表,吓出一身汗。
一个小跑题
顺便提一句,查询结果也别一股脑全返给模型。我有次没加 LIMIT,模型拉回来三万多行,token直接爆了,还慢。后来统一加 LIMIT 100,超了就提示用户缩小范围。
三道防线叠起来,这个查库智能体跑了俩月没出过事。模型那层我接的讯飞星辰 MaaS,现成大模型API,不用自建算力。