安全执行动态sql必须同时满足:标识符需白名单校验+quote_ident(),值参数必须用using;仅quote_ident()无法防注入,因不校验合法性;copy路径和ddl同样需白名单与权限限制。

直接拼接用户输入进 EXECUTE 字符串等于打开 SQL 注入大门;安全执行动态 SQL 的核心是:标识符(表名、字段名)必须白名单校验 + quote_ident(),值参数必须走 USING,二者缺一不可。
动态SQL中表名/字段名为什么不能只靠quote_ident()
quote_ident() 只做语法转义,不校验合法性。传入 'users; DROP TABLE accounts; --' 会被转成 "users; DROP TABLE accounts; --",仍是一个合法标识符,后续拼进 EXECUTE 就会执行恶意语句。
- 必须在调用
quote_ident()前,用正则强制校验格式,例如:IF input !~ '^[a-zA-Z_][a-zA-Z0-9_]{0,63}$' THEN RAISE EXCEPTION 'invalid identifier'; END IF; - 禁止对表名/字段名使用
quote_literal()—— 它加单引号,导致语法错误,如FROM 'users' -
format('%I', input)和quote_ident(input)效果等价,但同样绕不开白名单前置校验
值参数必须用USING,不能拼进字符串
哪怕表名已用 quote_ident() 处理,若 WHERE 条件里的值还用 || 拼接,照样注入。数据库不会把拼进去的字符串当数据,而是当 SQL 语法的一部分解析。
- 危险写法:
EXECUTE 'SELECT * FROM ' || quote_ident(t) || ' WHERE id = ' || user_id; - 正确写法:
EXECUTE 'SELECT * FROM ' || quote_ident(t) || ' WHERE id = $1' USING user_id; -
USING支持多个参数:USING val1, val2, val3,对应 SQL 中的$1,$2,$3 -
USING只能传值,不能传标识符(如表名、ORDER BY 字段),那些必须走白名单 +quote_ident()
COPY 路径和动态 DDL 是高危盲区
COPY 在函数里拼接文件路径是最常被忽略的任意文件读取入口。攻击者传入 /etc/passwd 就能直接泄露系统敏感文件。
- 绝对禁止:
EXECUTE 'COPY users FROM ''' || filename || ''''; - 动态 DDL(如
CREATE TABLE)必须同样遵守白名单 +quote_ident()规则,且应限制 schema 权限,避免创建到公共 schema - 如果必须支持任意路径导入,应改用服务端配置白名单目录 +
COPY FROM PROGRAM(需 superuser 权限,慎用)
真正难的不是记住 quote_ident() 或 USING,而是每次拼字符串前下意识问一句:这个变量来自哪里?它是否可能被用户控制?如果是,它属于标识符还是值?—— 漏掉一次判断,就可能绕过所有防护。










