SQL MCP Server 的只读模式为什么还需要数据库账号权限限制?

作者:袖梨 2026-09-13

SQL MCP Server 即使把每条查询放进 PostgreSQL 的只读事务,也仍然需要限制数据库账号权限。原因很直接:只读事务约束的是“不能修改什么”,账号权限约束的是“能够看见什么、能够执行什么、能够连接到哪里”。一个拥有全库读取权限的账号,在只读事务中依旧可以导出客户信息、业务秘密和认证数据;一个拥有危险函数执行权限的账号,也可能通过看似只读的 SELECT 触发超出预期的能力。

safe-postgres-mcp 提供了一个很清晰的纵深防御示例。它使用 PostgreSQL 原生 BEGIN TRANSACTION READ ONLY 拒绝写入,以扩展查询协议阻止一条 prepared statement 携带第二条命令,并增加语句超时、服务端游标行数上限和最终 ROLLBACK。项目文档同时明确要求 DATABASE_URL 指向专用的最小权限只读角色,因为服务保证的是只读和有界,不是完整的数据授权。

只读事务究竟保证什么

PostgreSQL 的只读事务会让数据库引擎在执行阶段拒绝 INSERT、UPDATE、DELETE、CREATE、DROP、TRUNCATE 等会产生持久修改的操作。即使应用层关键词检查被大小写、注释或复杂 CTE 绕过,数据库仍会返回只读事务错误。

这种保证比正则可靠,因为最终判定者是理解 PostgreSQL 语义的数据库引擎。应用不必穷举所有写入关键词,也不必准确预测未来版本新增的每一种语法。

项目仍保留关键词预检查,用于更快返回清晰错误,并专门检查 CTE 中隐藏的数据修改。文档正确地把它们称为用户体验和第二道防线,而不是核心安全保证。

只读和授权是两个维度

只读回答的是语句是否会修改数据库状态。授权回答的是当前身份是否可以访问某个数据库、schema、表、列、行、函数或系统目录。

如果 MCP 使用拥有所有表 SELECT 权限的账号,Agent 可以在只读事务中读取 users、payments、api_keys 和内部审计表。没有发生写入,不代表没有安全事故。

因此安全模型至少要同时满足两条:事务层拒绝修改,角色层只允许读取任务真正需要的数据。任何一条缺失,系统都不能称为安全的生产只读访问。

为什么提示词不能替代权限

系统提示可以要求模型只查询某些表,但它属于概率性行为约束。提示注入、用户诱导、上下文误解和模型错误都可能让 Agent 生成越界查询。

数据库 GRANT 与 REVOKE 是确定性执行边界。即使模型明确请求敏感表,数据库也会根据会话角色拒绝。

最小权限还能保护 MCP Server 自身被攻破的场景。攻击者即便获得该连接,也只能触达账号原本被授权的对象。

扩展查询协议防止堆叠语句

safe-postgres-mcp 将 Agent SQL 作为 prepared statement 通过 PostgreSQL 扩展查询协议发送。该协议的一条 Parse 消息要求单个语句,因此在分号后夹带第二条命令会被服务器拒绝。

项目还实现了理解字符串、注释和 dollar quote 的语句分割检查,以便在网络往返前给出明确错误。但不能把手写分割器当成唯一边界,真正可靠的是协议与数据库共同拒绝多命令。

这解决的是 statement stacking,不解决单条 SELECT 的数据访问范围。一条语句足以读取大量敏感数据,所以账号权限仍不可缺少。

CTE 中隐藏写操作

WITH 子句可以包含数据修改 CTE,例如先 DELETE 并 RETURNING,再由外层 SELECT 读取结果。只检查输入开头是 WITH 或最终是 SELECT 会造成漏判。

项目实现了隐藏写入扫描,并由只读事务兜底。即使扫描逻辑对某种注释或方言边界判断失败,数据库仍应以 SQLSTATE 25006 拒绝修改。

数据库账号若本身没有写权限,又提供第三道防护。三层控制分别覆盖解析器缺陷、事务配置错误和连接路径变化。

为什么最终总是 ROLLBACK

服务在每次查询后执行 ROLLBACK,不提交事务。这让会话状态和意外临时影响更容易清理,也避免未来代码改动不慎开启可写事务后自动提交。

ROLLBACK 不是权限控制。如果语句读取了敏感行并已经返回给模型,回滚无法收回这些数据;如果调用了具有外部副作用的函数,数据库事务也未必能撤销外部系统变化。

应把回滚理解为执行生命周期控制,而不是数据保密机制。

SELECT 也可能造成数据泄露

最明显的风险是 SELECT * FROM sensitive_table。它完全符合只读事务,却可能把个人信息、令牌哈希或商业数据送入模型服务和会话记录。

更隐蔽的方式是通过 JOIN、子查询、聚合和条件判断逐步推断数据。即使响应限制为数百行,攻击者也能分批翻页或反复提问。

safe-postgres-mcp 明确不做 PII 掩码,因此数据库端应使用列权限、脱敏视图、行级安全策略或专门的分析副本限制可见范围。

专用账号为什么重要

不要复用应用后台、开发者个人或数据库管理员账号。为 Agent 创建专用登录,名称、用途和负责人清晰,便于审计、轮换和紧急吊销。

专用角色只对目标数据库拥有 CONNECT,只对批准 schema 拥有 USAGE,只对批准表或视图拥有 SELECT。默认权限也要检查,避免以后新建表自动开放。

CREATE ROLE ai_reader LOGIN PASSWORD 'replace-with-secret';
GRANT CONNECT ON DATABASE analytics TO ai_reader;
GRANT USAGE ON SCHEMA reporting TO ai_reader;
GRANT SELECT ON reporting.daily_metrics TO ai_reader;

不要授予 SUPERUSER、CREATEDB、CREATEROLE、REPLICATION、BYPASSRLS 或 schema CREATE。密码通过秘密管理注入,不写进共享 MCP 配置。

PUBLIC 权限容易被忽略

PostgreSQL 的 PUBLIC 代表所有角色。即使没有显式给 ai_reader 授权,它仍可能继承数据库、schema 或函数对 PUBLIC 开放的能力。

审计时不能只查看该角色的直接 GRANT,还要检查 PUBLIC、角色成员关系、默认权限与对象所有者设置。

对敏感环境可收紧 public schema 的 CREATE,撤销不必要函数执行和数据库连接,再按用途显式授予。

角色继承会扩大边界

如果 ai_reader 是其他角色成员,默认 INHERIT 可能让它自动获得那些角色的权限。一个看似只读的新账号可能因历史成员关系拥有意外能力。

使用系统目录和 has_table_privilege 等函数核验有效权限,而不是只阅读建角色脚本。定期把实际授权导出并与基线比较。

生产 Agent 角色通常不需要 SET ROLE。若必须支持多租户角色切换,应在独立连接池中固定允许目标,避免模型任意选择高权限角色。

视图并不天然安全

脱敏视图是限制列和行的好工具,但其安全性取决于所有者、security_invoker、security_barrier 和底层函数。

视图若以高权限所有者执行,可能有意暴露底层数据,也可能因定义错误泄露敏感列。允许 Agent 访问前应把视图当成 API 接口审查。

只授予视图 SELECT,并撤销基础表权限。修改视图定义时运行回归测试,确认列、行过滤和租户隔离仍生效。

行级安全用于数据范围

同一张表可能包含多个租户或部门的数据。仅靠表级 SELECT 无法限制某个 Agent 只能读取自己的范围。

PostgreSQL Row Level Security 可以根据会话身份或受控上下文过滤行。连接角色不能拥有 BYPASSRLS,表所有者行为也要明确。

连接池复用时必须可靠设置并清理租户上下文。若上下文来自模型自由输入,Agent 可以冒充其他租户,应由宿主从已认证用户身份注入。

函数执行权限是关键盲区

SELECT some_function() 在语法上是读取语句,但函数可能写表、访问文件、发网络请求或以 SECURITY DEFINER 身份读取更高权限数据。

撤销不需要函数的 EXECUTE,审查允许 schema 中的函数,并限制 search_path。SECURITY DEFINER 函数必须固定安全搜索路径,避免对象名劫持。

扩展提供的函数也需要检查。不要因为接口只有 query 工具,就假设 SQL 能力只覆盖普通表读取。

系统目录会暴露哪些信息

list_schemas、list_tables 和 describe_table 会查询 PostgreSQL 系统目录。项目使用固定、参数化查询,避免把客户端标识符直接拼接进 SQL。

系统目录仍可能暴露对象名、所有者、索引、统计估算和业务结构。这些信息对攻击者有价值,也可能包含敏感命名。

账号权限和工具实现应确保只返回需要的非系统 schema。若不同团队共享实例,可为 Agent 准备隔离数据库或专用只读副本。

语句超时控制资源风险

项目默认把 QUERY_TIMEOUT_MS 设为五秒,允许配置但设置十二万毫秒硬上限。SET LOCAL statement_timeout 在事务内生效,慢查询由 PostgreSQL 取消。

超时可以阻止 pg_sleep 或昂贵查询长期占用连接,但不能保证五秒内的负载无害。多个并发复杂查询仍可能消耗大量 CPU、IO 和缓存。

应同时限制连接池、并发、数据库资源组和只读副本负载。监控超时率,并确认客户端不会立即无限重试。

服务端游标与行数上限

safe-postgres-mcp 使用服务端游标读取最多 maxRows 加一行,不让 node-postgres 先把无限结果全部缓存在内存。超过上限时停止并返回 truncated 标识。

默认上限为五百行,配置硬上限为一万行。即使查询自己写了更大的 LIMIT,服务端读取仍受限。

行数上限保护 MCP 进程和模型上下文,不限制数据库为计算这些行而扫描的数据量,也不阻止多次分页提取。因此它不能替代表、列和行级授权。

EXPLAIN 为什么拒绝 ANALYZE

explain_query 使用 EXPLAIN FORMAT JSON 返回计划,不执行目标查询。项目拒绝 ANALYZE 或 ANALYSE,因为这些选项会真正运行语句。

查询计划可能泄露表名、索引、过滤条件和统计估算,所以仍然受账号可见范围约束。

先解释再查询有助于发现高成本 JOIN 与全表扫描,但 Agent 不应把计划成本当成绝对运行时间。数据库统计过期时估算可能偏差很大。

错误配置必须启动失败

项目对 MAX_ROWS 和 QUERY_TIMEOUT_MS 做硬上限校验,垃圾值或缺失连接地址会让进程退出,而不是静默采用无限制配置。

这种失败关闭减少了拼写错误造成的安全降级。运维系统应对退出报警,不要在失败后自动替换成另一套高权限默认连接。

启动成功也不证明授权正确。部署检查必须实际调用 catalog 工具和查询工具,验证敏感对象不可见。

连接串本身是高价值秘密

DATABASE_URL 包含主机、数据库、用户名和密码。把它直接写进项目级配置可能随代码提交或被 Agent 读取。

使用秘密管理服务或进程环境注入,并限制日志、崩溃报告和进程列表暴露。项目通过 stderr 输出诊断,仍需确认错误不会包含完整连接串。

定期轮换凭据,设置短期有效期更好。发生异常查询时可以单独吊销 Agent 账号,不影响主应用。

只读副本是不是更安全

连接物理只读副本可以减少写主库风险,并隔离部分查询负载,但副本通常仍包含完整数据。

它不能解决敏感信息读取、慢查询、连接耗尽和提示注入。账号权限、超时与行数上限仍然需要。

分析副本最好进一步只同步批准数据或使用脱敏数据集。延迟也要向 Agent 说明,避免把旧结果用于实时决策。

提示注入不会被事务阻止

数据库字段可能包含自然语言指令,诱导模型泄露信息或调用其他工具。BEGIN TRANSACTION READ ONLY 只理解数据库操作,不理解返回内容。

宿主必须把查询结果标记为不可信数据,禁止它改变系统规则和工具授权。对发消息、执行命令、访问秘密等工具使用独立审批。

账号权限减少可用于注入的数据源,是防护链的重要一环。

如何验证核心安全声明

项目文档称单元与 MCP 接线测试覆盖安全解析,并有真实 PostgreSQL 集成测试。关键测试绕过文本过滤,直接在原始连接的只读事务中尝试 CREATE、INSERT、UPDATE、DELETE 和 TRUNCATE,期望数据库统一拒绝。

部署团队也应在自己的 PostgreSQL 版本、扩展和角色设置上复现。特别测试多语句、数据修改 CTE、注释、dollar quote、未闭合字面量和 EXPLAIN ANALYZE。

随后用同一账号直接连接数据库,确认敏感表、列、函数和其他 schema 都被权限拒绝。这一步才验证了账号安全边界。

权限验收清单

确认 Agent 使用独立 NOINHERIT 或成员关系清晰的登录角色,不具备超级用户、复制、建库、建角色和绕过行级安全能力。

确认只授予目标数据库 CONNECT、目标 schema USAGE、批准视图或表 SELECT;PUBLIC 与默认权限没有意外开放;基础敏感表不可直接访问。

确认不必要函数 EXECUTE 已撤销,SECURITY DEFINER 函数经过审查,search_path 固定,扩展能力受限。

确认事务只读、扩展协议单语句、超时、游标行数上限、回滚和配置硬上限均通过实际集成测试。

确认结果被视为不可信输入,连接串受秘密管理保护,数据库审计能够关联 MCP 会话和查询主体。

权限复核不能只做一次。每次新增 schema、视图、函数、扩展或角色成员关系后,都应重新以 Agent 账号执行可见性测试,并把结果纳入部署门禁。

safe-postgres-mcp 把只读保证放在 PostgreSQL 引擎中,是比仅扫描关键词更可靠的设计;扩展查询协议、超时、游标和回滚进一步缩小了风险。但这些机制不决定 Agent 有权读取哪些数据。只有再配置专用最小权限账号、视图或行级安全、函数权限和秘密管理,才能同时建立完整性、可用性与机密性边界。

相关文章

精彩推荐