gulmezeren2-byte/readonly-sql-guard

GitHub: gulmezeren2-byte/readonly-sql-guard

面向 AI agent 的 SQL 只读防护库,通过分析语句实际调用的函数而非仅匹配关键词,在 SQL 到达数据库前拦截伪装成查询的危险操作。

Stars: 0 | Forks: 0

# readonly-sql-guard **一个可证明为只读的 SQL 防护工具,适用于 AI agent —— 可直接插入任何 MCP server 或数据库工具。** [![PyPI](https://img.shields.io/pypi/v/readonly-sql-guard)](https://pypi.org/project/readonly-sql-guard/) [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/gulmezeren2-byte/readonly-sql-guard/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) 🇨🇳 中文版:[README.zh-CN.md](README.tr.md) 大多数数据库 MCP server 和 text-to-SQL 工具在强制执行“只读”时,仅检查语句的**形式** —— 它是否以 `SELECT` 开头?它是否包含单词 `DELETE`?这远远不够。一个格式完全正确的 `SELECT` 语句也可以读取文件、打开 socket、运行 shell、逃逸只读事务或改变 session: ``` SELECT pg_read_file('/etc/passwd') -- reads a server file SELECT * FROM dblink('host=attacker','SELECT 1') AS t(x int) -- opens an outbound socket SELECT * FROM orders; EXEC xp_cmdshell 'whoami' -- SQL Server shell COMMIT; DROP SCHEMA public CASCADE; -- escapes the read-only transaction ``` 在真实的 MCP server 中,前两种情况导致 [Anthropic 的参考 Postgres server 被归档](https://github.com/modelcontextprotocol/servers-archived/tree/HEAD/src/postgres),并将 Supabase 的 MCP 变成了教科书般的 [“致命三要素”](https://simonwillison.net/2025/Jul/6/supabase-mcp-lethal-trifecta/)。本库检查语句**调用**的函数,而不仅仅是其形式 —— 并对此提供证明。 ## 只读在这里是一个数字,而不是形容词 `readonly-sql-guard benchmark` 会运行 28 种格式正确的 SQL 攻击(包括上述按名称指出的两种)和 8 种合法读取,并将**相同的测试集**通过其他工具附带的快捷方式运行,以便你直观看到差距: | 防护工具 | 拒绝的攻击数 | 允许的合法读取数 | |---|---|---| | starts-with-`SELECT` 检查 | **6 / 28** | 8 / 8 | | write-keyword 黑名单 | **9 / 28** | 7 / 8 *(拦截了字符串中包含 "delete" 的读取)* | | **本防护工具** | **28 / 28** | **8 / 8** | 每个数字均根据实时运行结果计算得出。你可以将自己的攻击 payload 粘贴到[浏览器端 playground](https://gulmezeren2-byte.github.io/erp-report-engine/playground.html) 中(通过 Pyodide 运行相同的防护检查,数据不会被发送到任何地方)。 ## 安装 ``` pip install readonly-sql-guard # or: uv add readonly-sql-guard ``` 它唯一的依赖是 [`sqlglot`](https://github.com/tobymao/sqlglot)。 ## 使用 ``` from readonly_sql_guard import assert_read_only, ReadOnlyViolation assert_read_only("SELECT customer, SUM(net_total) FROM orders GROUP BY customer", dialect="postgres") # 返回 None — 这是一个读取 assert_read_only("SELECT pg_read_file('/etc/passwd')", dialect="postgres") # 引发 ReadOnlyViolation: function pg_read_file() 不是一个读取 ... ``` 对于你**并非**亲自编写的 SQL —— 即任何由 agent 或用户提供的 SQL —— 请使用 **strict mode**,它还会默认拒绝任何 parser 无法识别的函数: ``` try: assert_read_only(agent_sql, dialect="postgres", strict=True) except ReadOnlyViolation as e: return f"refused: {e}" # never reaches the database rows = cursor.execute(agent_sql) # now safe to run ``` `dialect` 接受 `postgres` / `mysql` / `tsql`(或 `mssql`/`sqlserver`)/ `sqlite` 以及一些别名;传入 `None` 则以方言无关的方式进行解析。如果你不想使用异常捕获机制,也可以使用 `is_read_only(sql, ...) -> bool`。 ## 在 MCP server 中使用 ``` @server.tool() def query(sql: str) -> dict: assert_read_only(sql, dialect="postgres", strict=True) # raises before we touch the DB return {"rows": run_query(sql)} ``` ## 防护原理 只读模式通过多层机制强制执行,每一层都精准对应其提供的保证: 1. **词法分析** —— 对字符串字面量被置空后的语句进行分析(引号值中的 keyword 是数据,而非代码):单条语句、`SELECT`/`WITH` 开头、无注释、无 write/DDL keyword、无 write-escalating lock hint、无调用引发副作用的函数。 2. **Parse-tree (`sqlglot`)** —— 语句**必须能被解析**,否则将被拒绝(一个无法读取 query 的防护工具自然无法为其担保 —— 而 `OPENROWSET` 恰好就是无法被解析的)。它必须是单条 read query,且不包含 `INSERT`/`UPDATE`/`DELETE`/DDL/`EXEC`/`INTO` 节点,也不调用任何黑名单函数。 3. **Strict mode** —— 此外还会拒绝任何 `sqlglot` 无法识别的函数。它的函数注册表即是白名单:它了解可移植的分析函数,但绝不包含任何读取文件或对外拨号的函数。 该防护工具经过**模糊测试**(基于 hypothesis),因此无论被列入黑名单的函数如何改变大小写、空格或参数形式,都会被拒绝 —— 它绝不是靠死记硬背测试集来通过检验的。 ## 复现测试 ``` readonly-sql-guard benchmark # the table above, computed live readonly-sql-guard check "SELECT 1" --dialect postgres ``` ## 项目背景 这是从 [**erp-report-engine**](https://github.com/gulmezeren2-byte/erp-report-engine) 中提取出的只读防护工具 —— 这是一个用于 ERP 后端数据库的只读报告与 MCP 层 —— 现已进行泛化处理,以便任何工具都能采用。该项目是其旗舰应用实例;而本库则是可复用的底层基元。 ## License MIT © Eren Gülmez
标签:AI代理, CISA项目, MCP, SQL防护, 只读校验, 大模型工具, 数据泄露, 逆向工具