动态表名和列名无法参数化是数据库引擎硬限制,必须通过白名单校验+QUOTENAME/sql.identifier转义实现安全拼接,严禁字符串拼接或正则过滤。

动态表名和列名无法参数化,这是数据库引擎的硬限制,不是写法问题。强行拼字符串或套用 QUOTENAME() / sql.identifier() 之前没做校验,等于直接交出数据库控制权。
为什么不能对表名/列名用参数占位符
PostgreSQL 报 ERROR: syntax error at or near "$1",SQL Server 报 Incorrect syntax near '@'——这些错误不是你写错了语法,而是数据库明确拒绝把占位符当标识符解析。参数化只作用于值(WHERE name = $1),而 FROM $1 或 ORDER BY $1 ASC 在语法层就不合法。
常见错误是看到报错后退一步改用字符串拼接:'FROM ' + @table_name,再加一层 REPLACE(@table_name, '''', '''''')。这毫无意义:分号、--、/*、甚至 ] 都能绕过单引号转义,直接触发语句截断或执行额外命令。
必须走白名单校验,且校验要落在拼接前
白名单不是可选项,是唯一安全路径。它要求你提前定义所有允许出现的表名、列名、schema 名,并在用户输入进入 SQL 构建流程前就完成比对。
- 校验逻辑必须独立于拼接逻辑,比如先执行
if (!allowedTables.includes(userTable)) throw new Error("Invalid table"),再调用sql.identifier(userTable) - 白名单要覆盖全部合法变体:如果业务允许
users和user_profiles,但不允许user_info,那后者哪怕字符合规也得拒掉 - 大小写敏感性要统一:数据库通常区分大小写(尤其 PostgreSQL),白名单条目必须与实际对象名完全一致,不能只做
toLowerCase() - 不要用正则代替白名单:
/^[a-zA-Z0-9_]+$/能放过logs; DROP TABLE logs--这种输入,因为分号和注释符不在该正则的否定范围内
QUOTENAME() 和 sql.identifier() 只是“最后一道转义”,不是“兜底方案”
QUOTENAME()(SQL Server)和 sql.identifier()(pg-sql2)的作用是把已确认安全的名称加上双引号或方括号,防止名称含空格、连字符等特殊字符导致语法错误。它们不负责过滤恶意内容,也不做语义校验。
典型误用:
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM ' + QUOTENAME(@user_input);
如果 @user_input 是 users; DROP TABLE users--,QUOTENAME() 会输出 [users; DROP TABLE users--],整个语句变成:
SELECT * FROM [users; DROP TABLE users--]
这不是安全的表名,这是语法错误——但错误发生在解析阶段,攻击未生效;可一旦攻击者输入的是 users] DROP TABLE logs--,QUOTENAME() 就会闭合右括号,让后面的内容逃逸出去。
正确用法只有一种:QUOTENAME() 或 sql.identifier() 的输入必须已是白名单校验通过的字符串,例如:
const tableName = allowedTables.find(t => t === userInput);<br>if (!tableName) throw Error("table not allowed");<br>const query = sql`SELECT * FROM ${sql.identifier(tableName)}`;排序字段、分组字段、LIMIT/OFFSET 数值同样适用同一规则
它们和表名列名一样,属于 SQL 语法结构,不是值。所以:
-
ORDER BY后的字段名必须映射到白名单列名,方向(ASC/DESC)必须用CASE或枚举校验,不能直拼 -
LIMIT后的数字虽是数值,但属语法位置,需强转为整数并做范围限制(如Math.min(100, Math.max(1, parseInt(input)))),不能直接拼进字符串 -
IN列表不能写成WHERE id IN ($1),必须根据数组长度动态生成占位符串($1, $2, $3),再逐个绑定参数
最容易被忽略的是:白名单校验必须覆盖所有动态拼接点,哪怕它看起来“只是个排序方向”。一个没校验的 ASC/DESC 输入,配合 ORDER BY 后的列名校验漏洞,就能组合成完整注入链。

















