XyraSinclair/pg-query-gate

GitHub: XyraSinclair/pg-query-gate

一个 PostgreSQL 不可信 SQL 服务的多层验证与会话加固库,通过 AST 门控、token 拦截、会话级 SET LOCAL 加固和最小权限角色四层机制,安全地开放近任意只读查询能力。

Stars: 0 | Forks: 0

# pg-query-gate 针对在 PostgreSQL 上提供不可信 SQL 服务的默认拒绝(deny-by-default)验证与会话加固。 这段代码提取自一个生产环境的公共 SQL 接口,该接口曾处理过来自匿名和已认证调用者的数以万计近乎任意的查询,这些都是针对实时 PostgreSQL 数据库执行的。我们将其发布,是因为它所回答的问题 —— *你如何允许陌生人及其代理在你的数据库上运行 SQL?* —— 如今正摆在每一位构建数据产品的开发者面前,而目前大多数可用的答案要么是“不要这样做”,要么就是使用正则表达式。 ## 模型:四个独立的层 没有任何一层是被单独信任的。每一层都足够小,可以独立进行审计,任何一层的漏洞都不会影响其他层的运作。 1. **AST 门控** (`Policy::validate`)。使用 [sqlparser-rs](https://github.com/apache/datafusion-sqlparser-rs) 进行解析,然后拒绝所有不属于受限只读语句的内容:详尽的语句类别匹配(按名称拒绝每一个写、DDL、DCL、session、transaction 和 cursor 类别),对 CTE、set 操作、派生表和 lateral join 进行只读遍历,拒绝 SELECT INTO 和 FOR UPDATE/SHARE,设置上限的仅限字面量 LIMIT/OFFSET,一个能够到达 AST 可以承载的每个 `Query` 节点的 visitor 核心(这样新的嵌套位置就不会漏网),强制执行嵌套 ORDER BY 的工作量限制,笛卡尔积和重言式 join(tautological-join)检测,以及拒绝 EXPLAIN ANALYZE。 2. **Token 门控**(同样位于 `Policy::validate`)。在 token 层面进行独立审查:受拦截的 schema 限定符,受拦截的前缀(`pg_*`, `binary_upgrade_*`),以及按名称拦截的标识符(`dblink_*` 和 `lo_*` 函数、advisory locks、sequence movers、XML 泄露助手等)。它同样适用于带引号的标识符,因此 `"pg_catalog"."pg_class"` 无法绕过纯文本列表,并直接拒绝 Unicode 转义标识符形式(`U&"..."`),从而防止转义字节夹带受拦截的名称。 3. **Session 电池组** (`session::build_setup_statements`)。每次查询前的 `SET LOCAL` 前言:role 切换、固定的 `search_path`(优先 `pg_catalog`)、statement 和 lock 超时、`work_mem` 上限、关闭并行、关闭 JIT、`temp_file_limit`、row-level-security 上下文三件套(三个变量必须全部设置或全部不设),以及 `transaction_read_only = on` —— 这样即使验证器存在漏洞,也无法将意外的写权限转化为数据修改。该“电池组”在查询的 transaction 内运行,并且必须在每次查询时运行;它是承载单次查询限制的层。 4. **Role DDL** (`sql/hardening.sql`)。数据库侧的底线:一个 NOLOGIN 最小权限服务 role、`REVOKE CREATE ON SCHEMA public`,以及对所服务 schema 中现有和未来对象的函数执行撤销。它为所服务的 schema 提供底线保障,而不是 `pg_catalog`,因此 token 和 AST 门控仍然是针对 catalog 函数的主要控制手段。 ## 用法 ``` [dependencies] pg-query-gate = { git = "https://github.com/XyraSinclair/pg-query-gate" } ``` Session “电池组”采用的是 `SET LOCAL`,因此它仅在显式 transaction 内生效:打开一个(`BEGIN`),运行该“电池组”,运行验证后的查询,然后执行 `COMMIT` 或 `ROLLBACK`。在 transaction 之外,每一个 `SET LOCAL` —— 包括只读标志 —— 都会被默默丢弃,单次查询的保护也就荡然无存。 ``` use pg_query_gate::{Policy, session}; let policy = Policy::default(); // strictest posture; grow allowlists deliberately policy.validate(user_sql)?; // Err(reason) refuses the query let role = session::SessionRole::Anonymous { role_name: "query_ro".into() }; let timeout = session::effective_statement_timeout_ms(&role, requested_ms); for stmt in session::build_setup_statements(&role, &[], timeout, &Default::default())? { tx.execute(&stmt, &[]).await?; // inside the query's transaction } // then run the validated query in the same transaction ``` 通过 `Policy` 字段,部署方可以定义自己受限的词汇表:内部有行数上限的表值函数、允许的 lateral 函数、需额外拒绝的 set-returning 函数,以及扩展的拦截列表。默认是一个空的允许列表(allowlist)—— 你添加的每一个条目都是你需要负责的攻击面。 另外包含:`query_shape_hash`(用于滥用群体分析的剔除字面量的 blake3 哈希)和人性化的解析错误。 ## 这个 crate 不做的事情 它不会连接 PostgreSQL,不会进行连接池管理,不会流式传输结果,不会进行速率限制,也不会计费。这些属于你服务的职责。`session` 中的常量记录了原始接口运行时的上限(行数、响应字节数和单单元格限制);请在你的执行器中强制执行这些限制。 ## 致谢 基于 [sqlparser-rs](https://github.com/apache/datafusion-sqlparser-rs) (Apache DataFusion)构建,其 visitor-derive 机制使得“通过构造覆盖每一个查询节点”的门控成为可能;同时基于 [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) 构建。 ## 许可证 在 Harvest License 下发布(参见 `LICENSE.md`):可自由使用、修改和销售。唯一的条件 —— 即 Harvest —— 是 Steward 每年至多可以询问一次该作品对你而言的价值,而你需要如实作答。你选择的回报可以是付款、贡献代码、发布你自己的作品,或者诚实地回答零。只有保持沉默才构成违约。
标签:PostgreSQL, Rust, SQL验证, Streamlit, 可视化界面, 数据库中间件, 测试用例, 网络流量审计, 访问控制, 输入过滤, 通知系统