pg_graphql本身不会导致SQL注入,因其不拼接SQL字符串,所有查询均通过PostgreSQL的PREPARE+EXECUTE预编译执行,用户输入仅作为绑定参数;风险仅存在于自定义resolver中手动拼接SQL时。

pg_graphql 本身不会因底层拼接导致 SQL 注入 —— 它不拼接 SQL 字符串,所有查询都走 PostgreSQL 的 PREPARE + EXECUTE 路径,用户输入仅作为绑定参数。真正风险只出现在你绕过 pg_graphql、自己写 resolver 时手动拼接 SQL 的地方。
pg_graphql 的参数化机制到底怎么工作的
pg_graphql 把 GraphQL 查询(比如 user(email: $email))直接翻译成带 $1 占位符的 SQL:SELECT * FROM users WHERE email = $1。然后调用 PostgreSQL 的 PREPARE 阶段确定语法结构,运行时只做参数绑定。
这意味着即使传入 "'; DROP TABLE accounts; --",PostgreSQL 也只当它是普通字符串值,不会执行语句。
- 字段名、表名、操作类型全部来自 Schema 静态定义,无法被变量动态控制
- 错误信息默认不暴露数据库细节(除非显式开启
debug模式) -
search_path必须限制,否则攻击者可能通过 schema 切换绕过 RLS 策略
手写 resolver 时最常踩的三个坑
问题不在 GraphQL 层,而在你用 pg、asyncpg 或其他驱动手动构造 SQL 的地方。
- 用
format()或%s拼接表名/字段名:query = f"SELECT {field} FROM {table}"—— 表名无法参数化,必须白名单校验 - 把用户输入直接塞进
WHERE条件而没用占位符:pg.query("SELECT * FROM users WHERE email = '" + email + "'") - 用
json_build_object()构造动态WHERE,但没过滤 key 名 —— 攻击者可传{"user_id': '1 OR 1=1--": ""}
什么时候该用参数,什么时候必须白名单
参数化只适用于值(value),不适用于标识符(identifier):表名、字段名、排序方向(ASC/DESC)、JOIN 类型等都不能用 $1 绑定。
- 值类输入(如
email、id、status)—— 全部走$1绑定 - 标识符类输入(如动态
ORDER BY field)—— 必须白名单校验,例如if field not in ["created_at", "name", "score"]: raise ValueError - 动态表名(如多租户分表)—— 必须映射到预定义规则,禁止任何用户可控字符串进入 FROM 子句
最容易被忽略的是:RLS(Row Level Security)策略本身不防拼接,它只在查询执行前生效;如果你的 resolver 手动拼了 SQL 并跳过 pg_graphql,那 RLS 就完全失效了。安全边界始终在「谁构造了最终 SQL」这一层,而不是「谁发了 GraphQL 请求」。











