PostgreSQL 16无“安全模式”,防SQL注入需严格遵守两条:标识符用quote_ident()或format('%I',...)并前置白名单校验,值一律用USING绑定;二者缺一不可。

PostgreSQL 16 本身没有叫“安全模式”的内置开关,动态 SQL 注入防护完全依赖你写法是否合规——核心就两条:quote_ident() 或 format('%I', ...) 处理标识符(表名、字段名),USING 绑定所有值,二者缺一不可。
动态表名/字段名必须白名单校验后再 quote_ident()
quote_ident() 不是防注入的银弹,它只做语法转义,不拒绝非法输入。比如传入 'users; DROP TABLE accounts; --',它会输出 "users; DROP TABLE accounts; --",而 PostgreSQL 会把它当一个合法(但危险)的标识符执行。
真正安全的做法是:在调用 quote_ident() 前,用正则强制校验格式:
IF input !~ '^[a-zA-Z_][a-zA-Z0-9_]{0,63}$' THEN RAISE EXCEPTION 'invalid identifier'; END IF;- 长度限制 64 字符,开头不能是数字,禁止特殊符号和空格
- 不要用
quote_literal()处理表名——它加单引号,导致语法错误 - 避免把用户输入直接塞进
format('%I', user_input),除非你已做过上述校验
所有查询值必须走 USING,严禁字符串拼接
哪怕表名已经用 quote_ident() 安全包裹,只要 WHERE 条件里的值是拼进去的,照样注入。例如:
EXECUTE 'SELECT * FROM ' || quote_ident(t) || ' WHERE id = ' || user_id; —— 危险
正确写法是把值作为参数传给 USING:
EXECUTE 'SELECT * FROM ' || quote_ident(t) || ' WHERE id = $1' USING user_id;-
USING支持多个变量:USING val1, val2, val3,对应 SQL 中的$1,$2,$3 -
USING只能传值,不能传表名、字段名、ORDER BY子句或函数名 - JSONB 键名也属于标识符,不能写成
f"data->>'{key}'",得用data->>%(key)s+ 参数字典(应用层)或白名单 +quote_ident()(PL/pgSQL)
COPY 路径、schema 名、ORDER BY 字段是高危盲区
这些地方最容易被忽略,但风险极高:
-
COPY的文件路径绝对禁止拼接:EXECUTE 'COPY users FROM ''' || filename || ''''等于开放任意文件读取,攻击者可传/etc/passwd -
search_path若由用户控制,可能绕过 RLS;应显式指定 schema,如myschema.users,而非依赖SET search_path TO ... -
ORDER BY动态字段无法参数化,必须硬编码白名单:IF sort_field NOT IN ('created_at', 'status', 'score') THEN RAISE EXCEPTION ...;,再拼接 - 多表 JOIN 场景下,RLS 不自动传播到关联表,若
billing_info无 RLS,即使accounts启用了也会泄露数据
最常被踩的坑不是不会写 quote_ident(),而是以为写了它就万事大吉;也不是忘了 USING,而是只对主查询用、漏了子查询或 CTE 中的值绑定;更隐蔽的是,把 JSONB 键名、CTE 别名、IN 子句里的数组元素,都当成普通值去拼接——它们其实都是 SQL 结构的一部分,该走白名单的必须走白名单。

















