Hotdry.

Article

Agent 的 run_sql 工具:PostgreSQL 会话沙箱、超时参数与 RLS 多租户隔离

针对 Coding Agent 与 MCP 数据库工具中「模型生成 SQL」的副作用与资源耗尽风险,说明如何用专用角色、会话级 GUC、连接池 checkout 钩子与行级安全策略组合成可落地的只读沙箱,并给出生产参数表与常见绕过边界。

2026-07-07database

当 Agent 暴露 run_sqlquery_database 或 NL2SQL 类 MCP 工具时,平台进程往往持有比普通应用更高的数据库可达性:同一连接串可能触达分析库、含 PII 的副本,甚至与业务服务共享的 PostgreSQL 实例。模型输出的 SQL 属于不可信程序—— 除经典注入(拼接 '; DROP)外,只读查询仍可造成全表顺序扫描、长时间持锁、COPY (SELECT …) TO STDOUT 拉取大量行导致内存与出口带宽被打满。PostgreSQL 文档明确建议:statement_timeoutlock_timeoutidle_in_transaction_session_timeout 等参数应在会话级设置,而非全局写入 postgresql.conf,以免影响运维会话与迁移任务。本文讨论在不改动业务表结构语义的前提下,用角色权限 + 会话 GUC +(可选)RLS 为 Agent 工具链搭一层数据库侧边界;不覆盖「完全禁止任意 SQL、仅允许预注册模板」的强沙箱(该路线成本更高但更安全)。

问题背景:工具链把「DBA 能力」交给了不可信文本

典型数据流:

  1. 编排器向模型注册 SQL 工具,描述中常写「只读」「仅 SELECT」—— 这不是数据库强制约束。
  2. 模型返回一条或多条 SQL 字符串;网关可能做字符串黑名单后,用应用主库连接或只读副本连接执行。
  3. 结果集序列化进上下文,驱动下一轮推理;失败时错误信息(含表名、列名)可能反喂模型,形成信息泄露通道。

与面向人类的 BI 产品不同,Agent 场景还有:

  • 高频重试:校验失败或空结果会触发多轮 run_sql,连接池与 max_connections 易被短时打满。
  • 长事务习惯:部分框架在 tool 调用前后未显式 COMMIT,会话停留在 idle in transaction,阻塞 vacuum 并占用连接(idle_in_transaction_session_timeout 正是为此设计)。
  • 多租户混用:若所有租户共用一个 agent_reader 角色且无 RLS,一次提示注入可能读到邻租户行。
  • 副本与主库混用:分析 Agent 连只读副本仍可对副本施加极高 I/O;若误连主库且权限过宽,只读标志也挡不住 INSERT(除非权限与 GUC 同时收紧)。

因此需要把防御拆成:身份(角色)会话行为(GUC)数据范围(RLS)网关策略(语句形态与结果上限) 四层,且与 MCP/HTTP 工具的 SSRF、写操作审批正交。

可落地实现:角色、池化 checkout 与会话 GUC

1. 专用角色与权限基线

为 Agent 单独建角色,不要复用应用 runtime 的数据库用户。

CREATE ROLE agent_sql_runner NOLOGIN;
REVOKE ALL ON SCHEMA public FROM agent_sql_runner;
GRANT USAGE ON SCHEMA app_analytics TO agent_sql_runner;
GRANT SELECT ON ALL TABLES IN SCHEMA app_analytics TO agent_sql_runner;
ALTER DEFAULT PRIVILEGES IN SCHEMA app_analytics
  GRANT SELECT ON TABLES TO agent_sql_runner;

建议同步落实:

建议 说明
登录载体 LOGIN 角色 agent_pool_loginGRANT agent_sql_runner 应用持登录角色,执行前 SET ROLE agent_sql_runner
CREATE 对 Agent 可见 schema 显式 REVOKE CREATE PostgreSQL 15+ 默认撤销 PUBLICpublic 上的 CREATE,其他 schema 仍需手工收紧
扩展 不授予 pg_execute_server_program 等危险权限;不安装 dblink/file_fdw 给 Agent 角色 扩展函数可能打开出站或读文件路径
序列 只读分析通常 GRANT SELECT 即可;避免 USAGE on sequence 若担心侧信道 视审计要求而定
视图 优先只授视图不授基表 视图可隐藏列;须检查 security_invoker / 定义者权限(见风险节)

default_transaction_read_only 设为 on 时,事务默认拒绝写语句;PostgreSQL 说明该参数在每个事务开始时继承,后续修改等价于 SET TRANSACTION。应对每条工具调用使用短事务BEGIN READ ONLY → 单条查询 → COMMIT

2. 会话级超时与只读 GUC(推荐默认值)

下列参数应通过 ALTER ROLE agent_sql_runner SET … 或连接建立后 SET / SET LOCAL 生效(单位均为毫秒,除非使用带单位的字符串形式):

参数 生产建议 作用
default_transaction_read_only on 默认只读事务
statement_timeout 15000(15s);重型分析租户可单独角色调到 60000 终止运行过久的单条语句
lock_timeout 3000(3s) 避免排队等锁拖住 Agent 会话
idle_in_transaction_session_timeout 60000(60s) 清理 tool 异常未提交事务
transaction_timeout 30000(30s,PG 14+) 限制单事务总时长;文档注明若 ≤ statement_timeout 则较长者被忽略
search_path app_analytics(显式 schema 列表,不含 $user 降低搜索路径劫持风险
row_security on(默认) 强制 RLS 生效;勿对 Agent 会话关闭

角色级设置示例:

ALTER ROLE agent_sql_runner SET default_transaction_read_only = on;
ALTER ROLE agent_sql_runner SET statement_timeout = '15s';
ALTER ROLE agent_sql_runner SET lock_timeout = '3s';
ALTER ROLE agent_sql_runner SET idle_in_transaction_session_timeout = '60s';
ALTER ROLE agent_sql_runner SET transaction_timeout = '30s';
ALTER ROLE agent_sql_runner SET search_path = app_analytics;

网关在每个 tool 调用事务内可再收紧(SET LOCAL 仅当前事务有效):

BEGIN READ ONLY;
SET LOCAL statement_timeout = '10s';
SELECT set_config('app.tenant_id', 'tenant-uuid', true);  -- 事务级 GUC,供 RLS 使用
-- 执行单条受控 SELECT
COMMIT;

3. 连接池:PgBouncer 与 checkout 钩子

应用侧连接池(Go database/sql、HikariCP、node-pg)应在 checkout 时重置会话状态,避免上一请求残留 SET ROLEsearch_path

  1. DISCARD ALL(或至少 RESET ALL + CLOSE ALL);
  2. SET ROLE agent_sql_runner
  3. 可选:再次 SET 关键 GUC(若池模式导致角色级 SET 未生效)。

PgBouncer transaction pooling 下,会话级 SET 不能假设跨事务保留;官方文档要求在事务池模式下于每个事务重新应用设置,或使用 connect_query(需评估安全与可维护性)。对 Agent SQL 工具更稳妥的是:

池模式 建议
Session pooling 适合 Agent 短连接 burst;注意池大小与 max_client_conn
Transaction pooling 必须在每条 SQL 外包 BEGIN/COMMIT 且事务内 SET LOCAL
Statement pooling 不推荐用于需要 GUC/RLS 的 Agent 查询

池容量建议与 Agent 并发解耦:

配置 建议值 说明
每租户最大并发 SQL 工具 2 防止单 run 多步刷库
max_size ≤ 租户并发 × 副本数 max_connections 留 20% 给人工与批作业
单结果集行数上限 500(网关截断) 超出则只返回前 N 行 + truncated: true
单单元格字节上限 4 KiB 防止 JSON 列撑爆上下文
单 tool 调用语句数 1 禁止多语句字符串;网关用解析器拒绝 ; 后第二语句

4. RLS 多租户:会话 GUC + 策略

PostgreSQL 行级安全文档说明:表启用 ENABLE ROW LEVEL SECURITY 后,普通 SELECT/UPDATE 须通过 policy;表属主默认不受 RLS 约束。Agent 角色必须是非属主SELECT 授予,策略才能生效。

ALTER TABLE app_analytics.events ENABLE ROW LEVEL SECURITY;
CREATE POLICY agent_tenant_select ON app_analytics.events
  FOR SELECT TO agent_sql_runner
  USING (tenant_id = current_setting('app.tenant_id', true)::uuid);

网关在事务开始时:

SELECT set_config('app.tenant_id', $1, true);  -- true = LOCAL to transaction

注意:current_setting(..., true) 在缺失时返回 NULL 而非报错;生产应约定未设置 tenant GUC 则拒绝执行,由网关先校验租户上下文,而不是依赖隐式空结果。

5. 网关层语句形态(与 DB 沙箱互补)

数据库 GUC 无法把「只允许 SELECT」变成语法定理。建议在网关增加轻量规则(可用 pg_query 解析 PostgreSQL 语法树,或受限方言解析器):

规则 处置
仅允许 SelectStmt 拒绝 INSERT/UPDATE/DELETE/Copy/Call
拒绝子查询中的 FOR UPDATE 避免锁争用
拒绝访问 pg_cataloginformation_schema(可选白名单) 降低 schema 枚举
强制 LIMIT 注入 若 AST 无 LIMIT,追加 LIMIT 500

只读副本上仍应使用同一套角色与 statement_timeout,避免分析查询拖垮副本延迟。

风险与边界

default_transaction_read_only 不是权限替代品。 角色若被误授 INSERT,只读事务会在执行时失败,但已暴露错误信息;应依靠 GRANT 最小化而非仅靠 GUC。

属主与 SECURITY DEFINER 绕过。 表属主不受 RLS 约束;SECURITY DEFINER 函数以定义者权限执行,可能绕过 Agent 角色的 RLS。Agent 角色不应能 EXECUTE 此类函数,除非逐函数审计。

昂贵只读查询。 statement_timeout 终止语句,但高并发短查询仍可能打满 CPU;需结合网关并发配额、副本隔离与 pg_stat_statements 告警。

COPY TO STDOUT 在只读事务下仍可能导出大量行;除 LIMIT 外,应在网关限制结果序列化大小,并视情况拒绝 CopyStmt

错误信息泄露。 PostgreSQL 错误含关系名、约束名;回灌模型前应脱敏或映射为错误码(与 MCP Tool Execution Error 策略一致)。

Transaction pooling 与 SET 漂移。 若未 SET LOCAL 或 checkout 未 DISCARD ALL,可能出现租户 GUC 串租;属高危配置错误,需集成测试覆盖。

解析器与方言差距。 网关 AST 规则可能漏掉新型语法或扩展语句;数据库侧角色权限仍是最后防线。

人工运维会话勿共用 Agent 角色。 ALTER ROLE … SET statement_timeout 会影响该角色发起的所有会话;运维应使用独立角色。

参考来源

database

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com