参数化只适用于值,表名、列名、排序方向等语法成分不可参数化,必须白名单校验后拼接;IN列表需动态生成占位符并逐个绑定,存储过程须用sp_executesql配合QUOTENAME()和显式参数声明。

所有动态值必须进参数,但表名、列名、排序方向这些语法成分不能参数化——这是数据库引擎的硬限制,不是写法问题。绕开它去拼字符串,等于主动打开注入大门。
哪些地方能用参数,哪些绝对不能
参数化只对“值”有效:WHERE 条件里的 id、name,INSERT 的字段值,UPDATE 的 SET 内容,IN 列表里的每个元素(比如 WHERE status IN (@p1, @p2, @p3))。但以下内容数据库根本不允许占位:
-
ORDER BY后的列名或ASC/DESC -
FROM或JOIN后的表名 -
SELECT后的列别名或计算字段名 -
LIMIT/OFFSET的数值(虽是数字,但属语法结构,需强转+范围校验)
试图对这些做 @tablename 占位,会直接报错 Incorrect syntax near '@' 或类似提示。
动态列名和排序字段必须走白名单校验
用户传 sort=created_at&order=desc,你不能直接拼成 ORDER BY created_at DESC。得先映射到预设安全集合:
var allowedSortCols = new Dictionary<string, string> {
["name"] = "user_name",
["time"] = "created_at",
["status"] = "status_code"
};
var sortCol = allowedSortCols.GetValueOrDefault(userSortInput, "id");
var sortOrder = userOrderInput?.ToUpper() == "DESC" ? "DESC" : "ASC";再拼进 SQL:ORDER BY {sortCol} {sortOrder}。注意这里用的是字符串拼接,但输入已被严格约束在已知合法范围内。
常见错误是只校验 ASC/DESC,却放行任意 sort 值——攻击者塞个 sort=id; DROP TABLE logs; -- 就能触发注入。
IN 列表怎么安全处理变长参数
不能写 WHERE id IN (@ids) 期望它自动展开,数据库不支持。正确做法分两步:
- 前端或服务端把用户传的 ID 数组(如
[1, 5, 9])转成固定长度的参数占位符串:@p0, @p1, @p2 - SQL 模板里写死对应数量的占位符:
WHERE id IN (@p0, @p1, @p2) - 循环调用
cmd.Parameters.Add()绑定每个值,类型明确(别用AddWithValue)
如果 ID 数量不确定,就拆成多个查询,或改用临时表 + 表值参数(SQL Server)/ UNNEST(PostgreSQL),避免拼接。
存储过程里动态 SQL 必须用 sp_executesql
写 EXEC(@sql) 等于放弃防御。正确路径是:
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM ' + QUOTENAME(@table_name) + ' WHERE status = @status';
EXEC sp_executesql @sql,
N'@status INT',
@status = @input_status;关键点:
-
@table_name用QUOTENAME()包裹,防标识符注入(但仅限已确认合法的表名,否则先查sys.tables) -
@sql字符串里只拼结构,所有值全走sp_executesql的参数列表 - 参数声明字符串(第二个参数)必须显式写类型,不能靠猜测
最容易被忽略的是:哪怕只动态一个表名,其余全是参数,只要用了 EXEC(@sql),整个查询就失去参数化保护。安全边界在执行入口,不在模板里写了几个 @。

















