format() 本身不安全,必须配合白名单校验和USING才能防注入;仅用%I或%L是误区,因它只转义不校验,无法拦截非法标识符或恶意SQL片段。

format() 本身不安全,必须配合白名单校验和 USING 才能防注入。只用 format('%I', user_input) 或 format('%L', user_input) 是常见误区——它不拦截非法标识符,也不绑定运行时值,纯属字符串替换。
为什么 format('%I', ...) 不等于 SQL 安全
format('%I', ...) 只做转义:加双引号、处理保留字、转义特殊字符。但它完全不校验输入是否合法。比如传入 'users; DROP TABLE accounts; --',format('%I', ...) 输出 "users; DROP TABLE accounts; --",仍是语法合法的标识符,EXECUTE 时照常执行恶意语句。
- 它不是白名单机制,无法拒绝非法字符(如分号、连字符、控制字符)
- 它不能替代
quote_ident()的前置校验逻辑,两者定位不同 - 在触发器或通用函数中直接用
format('%I', TG_TABLE_NAME)风险极低——因为TG_TABLE_NAME是系统变量,但若该值来自用户参数(如函数入参table_name TEXT),就必须先校验
安全拼接表名/字段名:白名单 + format('%I', ...)
标识符(表名、列名、模式名)必须走「先校验、再转义」两步。白名单正则应在 EXECUTE 前完成,不能省略。
- 校验示例:
IF table_name !~ '^[a-zA-Z_][a-zA-Z0-9_]{0,63}$' THEN RAISE EXCEPTION 'invalid table name'; END IF; - 校验通过后才调用:
format('SELECT * FROM %I WHERE %I = $1', table_name, col_name) - 禁止对字段名用
%L:format('WHERE name = %L', user_input)会加单引号,导致语法错误;值要用USING,不是%L
安全传值:USING 绑定,禁用 %L 拼接
所有用户可控的值(WHERE 条件、INSERT 字段值、ORDER BY 表达式中的数值)必须用 USING,不能塞进 format() 的 %L 占位符里。
- 危险写法:
EXECUTE format('SELECT * FROM users WHERE id = %L', user_id)—— 若user_id是字符串O'Reilly,结果语法错误;若是1 OR 1=1,就变布尔盲注 - 正确写法:
EXECUTE format('SELECT * FROM %I WHERE id = $1', 'users') USING user_id -
USING支持多参数:USING val1, val2, val3对应 SQL 中的$1,$2,$3,NULL 和类型自动处理 -
USING不能传标识符:表名、列名、ORDER BY子句仍需靠白名单 +%I
COPY 路径、动态 DDL 是 format() 的禁区
format() 不能用于构造 COPY 文件路径或任意 DDL 语句(如 CREATE TABLE ... 名称),这类操作一旦拼接用户输入,等于开放任意文件读取或元数据破坏。
- 绝对禁止:
EXECUTE format('COPY users FROM %L', filename)—— 攻击者传/etc/passwd直接泄露系统文件 - DDL 若必须动态,表名/列名仍需白名单校验,且函数应以最小权限角色运行(避免
postgres账户执行) -
format()在日志记录、审计触发器等场景可放心用,前提是所有动态部分(TG_TABLE_NAME,TG_OP,to_jsonb(NEW))均来自可信上下文
真正容易被忽略的是:白名单校验必须在 EXECUTE 之前,且不能只靠数据库层报错兜底——很多注入利用的是语法合法但语义越权的行为,错误日志里甚至不报异常。

















