graphql resolver中直接拼接sql字符串=主动开后门,必须禁用;安全路径是全程参数化+白名单校验表名列名等标识符+最小权限与rls。

GraphQL Resolver 中直接拼接 SQL 字符串 = 主动开后门,必须禁用;安全调用数据库的唯一可靠路径是全程参数化 + 白名单校验 + 最小权限。
resolver 里别用字符串拼接 SQL
这是最常见、最高危的入口。只要出现 f"SELECT * FROM {table} WHERE name = '{args.name}'" 或 "WHERE id = " + args.id 这类写法,单引号、分号、UNION SELECT 全部放行,和传统表单注入无异。
- pg_graphql 等封装层本身已强制走
PREPARE + EXECUTE,但自定义 resolver 绕过它时,就得自己扛住这道关 - 类型检查(如
typeof args.id === 'string')拦不住"1 OR 1=1"这种绕过 - 哪怕字段看着是数字,也不能
parseInt()后拼进 SQL —— 攻击者传的是字符串,不是整数
所有值必须用驱动原生占位符
用户输入只能作为绑定参数,不能进入 SQL 解析器。不同驱动语法不同,但原则一致:值走参数,SQL 结构固定。
- PostgreSQL(
pg):用$1,$2占位,传数组:client.query('SELECT * FROM users WHERE id = $1', [args.id]) - MySQL(
mysql2):用?占位:connection.execute('SELECT * FROM logs WHERE type = ?', [args.type]) - SQL Server(
mssql):用命名参数:request.input('id', sql.Int, args.id) - Prisma、Knex、Drizzle 等 Query Builder 不是免检金牌:禁用
prisma.$queryRaw、.whereRaw、sql`...${args.email}`这类“伪参数化”接口
表名、列名、ORDER BY 字段必须白名单校验
这类标识符(identifier)无法参数化,PostgreSQL 明确不支持 $1 替换表名,所以必须提前约束范围。
- 排序字段:
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 中 - 别用
.replace(/'/g, "''")或正则过滤后拼接 —— 绕过方式太多,且维护成本高
权限和 RLS 没配好,参数再安全也白搭
即使 resolver 100% 参数化,如果数据库用户有全库 SELECT 权限,攻击者仍可通过合法查询读取任意表。
- 执行
REVOKE SELECT ON public."Account" FROM api_user,让敏感表根本不出现在 GraphQL Schema 中 - 开启 RLS(Row Level Security)并配好策略,比如
CREATE POLICY user_read_own ON users FOR SELECT USING (id = current_setting('app.current_user_id')::uuid) - Schema 声明
id: ID!只保证非空和格式,不阻止 SQL 注入字符 —— GraphQL 类型系统不干预 resolver 内部逻辑
真正容易被忽略的点是:白名单校验和 RLS 都得落到具体数据表与业务上下文,不能只靠“参数化”自我安慰;一个没校验的 args.tableName 或一条没配 RLS 的 users 表,就足以让整个防护链失效。











