postgresql函数中动态sql需严格区分标识符与值:表名列名等用quote_ident()转义,值参数必须通过using子句绑定,禁用字符串拼接;函数应默认使用security invoker并限制权限。

PostgreSQL函数里拼接字符串就是高危操作
直接用 EXECUTE + 字符串拼接构造动态SQL,是PostgreSQL函数中最常见的注入入口。比如把用户传入的字段名、表名、排序方向硬塞进SQL字符串里,数据库会原样执行——攻击者就能插入 ;、UNION SELECT 甚至调用 pg_sleep() 探测或拖库。
- 所有用户可控输入(哪怕只是
text类型参数)都不能直接拼进EXECUTE的SQL字符串中 -
format()函数的%I占位符只安全用于标识符(表名、列名、函数名),不能用于值;%L才用于字面量值,但它会加单引号,不适用于数字或布尔等非字符串类型 - 用
quote_ident()处理标识符,用quote_literal()或quote_nullable()处理值,比format()更明确、更不易错
如何安全地传入表名、列名这类标识符
PostgreSQL不支持把表名或列名作为参数绑定到预处理语句里,所以必须靠转义。但 quote_ident() 是唯一可信赖的方式——它会把非法字符转成双引号包裹的合法标识符,比如输入 users; DROP TABLE accounts; 会被转成 "users; DROP TABLE accounts;",作为字段名时完全无害。
- 只对明确知道是标识符的参数用
quote_ident(),比如table_name、order_by_col - 禁止用
quote_ident()处理值,它不会加引号,直接拼进去会导致语法错误或注入 - 如果必须支持多表联合,且表名来自白名单,优先用
CASE分支硬编码,而不是动态拼接
值参数必须走 USING 子句,而不是字符串拼接
哪怕你已经用 quote_literal() 包裹了值,只要它是拼进SQL字符串里的,就仍可能被绕过(比如通过Unicode变体或嵌套注释)。真正的安全边界是 USING:它让PostgreSQL内部把参数当纯数据处理,完全剥离执行上下文。
-
EXECUTE 'SELECT * FROM users WHERE id = $1' USING user_id;—— 正确 -
EXECUTE 'SELECT * FROM users WHERE id = ' || user_id;—— 错误,整数也能被注入(如user_id := '1 OR 1=1') -
USING后面只能跟变量名,不能是表达式;多个参数用逗号分隔,顺序与$1,$2严格对应
函数权限和执行上下文容易被忽略
即使SQL写得再安全,如果函数用 SECURITY DEFINER 创建,又赋予了高权限角色(比如 postgres),那攻击者只要触发函数,就能借它的权限做任何事。更隐蔽的是,函数内调用其他函数时,若那些函数没做输入校验,也会成为跳板。
- 默认用
SECURITY INVOKER,让函数以调用者权限运行,最小化爆炸半径 - 若必须用
SECURITY DEFINER,函数开头立刻用SET search_path TO pg_catalog;防止恶意schema劫持 - 避免在函数里调用未经审查的第三方或动态生成的函数名
真正麻烦的不是写对一行 EXECUTE,而是整个函数里所有路径都得守住同一道防线:标识符走 quote_ident(),值走 USING,权限缩到最紧,连临时表名、序列名这些边缘输入也不能放行。











