pg_graphql本身不触发SQL注入,因其全程使用PREPARE+EXECUTE预编译,用户输入仅作绑定参数;真正风险在于手写resolver时手动拼接SQL字符串,必须用占位符处理值、白名单校验标识符,并配置RLS与最小权限。

pg_graphql 本身不会触发 SQL 注入,因为它全程走 PostgreSQL 的 PREPARE + EXECUTE 路径,用户输入只作为绑定参数;真正风险只出现在你手写 resolver 并手动拼接 SQL 字符串时。
resolver 中别用字符串拼接 SQL
这是最直接、最高危的入口。只要出现 f"SELECT * FROM {table} WHERE name = '{args.name}'" 或 "... WHERE id = " + args.id 这类写法,就等于把单引号、分号、UNION SELECT 全部放行。
- 所有值(
WHERE条件、IN列表、LIKE模式)必须用驱动原生占位符:PostgreSQL 用$1,MySQL 用?,SQL Server 用@param - 禁止对任何用户输入做
.replace(/'/g, "''")或正则过滤后拼接——绕过方式太多,且维护成本高 - 哪怕字段看着是数字(如
id: "123"),也不能直接parseInt()后拼进 SQL,因为攻击者可传"1 OR 1=1",类型检查拦不住
表名、列名、ORDER BY 字段不能参数化,必须白名单校验
PostgreSQL 不允许用 $1 替换表名或排序字段,所以这类标识符(identifier)必须提前约束范围,不能来自用户输入。
- 排序字段:
allowed_sort_fields = {"created_at", "name", "status"},检查if args.sortBy not in allowed_sort_fields - 动态查询字段:
SELECT {field} FROM users中的{field}必须从预定义集合取,比如{"id", "email", "full_name"} - 表名映射建议用字典硬编码:
table_map = {"user": "public.users", "post": "content.posts"},再校验 key 是否在 dict 中
用 Query Builder 也要警惕“伪参数化”陷阱
Prisma、Knex、Drizzle 等工具不是免检金牌。它们内部安全,但暴露的 raw 接口仍会绕过防护。
-
Prisma:避免prisma.$queryRaw`SELECT * FROM users WHERE name = ${args.name}`,改用findMany({ where: { name: { contains: args.name } }) -
Knex:禁用.whereRaw("name LIKE '%?%'", [args.q]),改用.where('name', 'like', `%${args.q}%`) -
Drizzle:优先用eq(users.email, args.email),而非sql`... WHERE email = ${args.email}`
权限和 RLS 没配好,参数再安全也白搭
即使 resolver 100% 参数化,如果数据库用户有全库 SELECT 权限,攻击者仍可通过合法查询读取任意表。
- 执行
REVOKE SELECT ON public."Account" FROM api_user,让敏感表直接不出现在 GraphQL Schema 中 - 对多租户表强制启用 RLS:
CREATE POLICY tenant_isolation ON users FOR SELECT USING (company_id = current_setting('app.current_tenant')::uuid) - JWT claims 必须映射到 PostgreSQL 角色,否则
current_user始终是固定角色,RLS 不生效
最容易被忽略的是:字段名/表名校验和 RLS 配置这两步,既不产生运行时开销,也不影响开发体验,但一旦漏掉,前面所有参数化努力都会归零。

















