sp_executesql是唯一安全选择,因其在编译阶段分离代码与数据,参数值不参与语法解析;而exec拼接字符串会使用户输入直接进入解析器,导致sql注入。

sp_executesql 能防注入,不是因为它名字里带“sql”,而是它把代码和数据在 SQL Server 编译阶段就物理隔离。用 EXEC 拼字符串,等于把用户输入直接喂进解析器;而 sp_executesql 让 SQL Server 先编译模板、再绑定值——值永远不参与语法分析。
为什么 EXEC(@sql) 一拼就崩?
只要字符串里出现 + 拼接,哪怕只拼一个变量,风险就等同裸写 SQL:
-
EXEC('SELECT * FROM users WHERE id = ' + @id)→ 攻击者传@id = '1; DROP TABLE users; --',整条语句被当命令执行 - 即使
@id是INT类型,SQL Server 在拼接时也会隐式转成字符串,失去类型保护 -
REPLACE(@id, '''', '''''')这类过滤只防单引号,挡不住1; WAITFOR DELAY '0:0:5'这类延迟注入
sp_executesql 的三要素缺一不可
漏掉任一环,就退化成 EXEC 级别风险:
-
@stmt字符串里不能含任何用户值:正确是N'SELECT * FROM t WHERE status = @s',错误是N'SELECT * FROM t WHERE status = ' + @s -
@params必须显式声明类型和长度:要写N'@s TINYINT'或N'@name NVARCHAR(50)',不能只写@s - 参数值必须用
@param = @value形式传入:写成@s = @input才对,写成@s = 'abc'(字符串字面量)或直接传变量值都算违规
动态对象名(表名/列名)为什么 QUOTENAME 不够?
QUOTENAME(@table) 只做标识符转义,比如把 users 变成 [users],但它不校验内容合法性:
- 传入
@table = 'orders; DROP TABLE products; --',QUOTENAME会输出[orders; DROP TABLE products; --],仍可触发语句级注入 - 真正安全的做法是白名单校验:
IF @table NOT IN ('orders', 'products', 'users') THROW 50000, 'Invalid table', 1 - 或查系统视图确认存在且归属预期 schema:
IF NOT EXISTS (SELECT 1 FROM sys.tables t JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE s.name = 'dbo' AND t.name = @table)
从数据库读出的数据再拼 SQL,算可信吗?
不算。哪怕是从 sys.tables 或业务表里 SELECT 出来的字段,只要后续要拼进新 SQL,就必须重检:
- 高权限用户可能篡改元数据;误操作也可能写入非法值(比如把
table_name字段 UPDATE 成恶意字符串) - 常见翻车点:
SELECT @col = column_name FROM sys.columns WHERE object_id = @obj_id; SET @sql = 'SELECT ' + @col + ' FROM ...'—— 这里@col是从库读的,但没白名单兜底 - 正确做法:先判断
@col IN ('id', 'name', 'status'),再QUOTENAME(@col),最后拼
最易被忽略的点是:人总以为“进了存储过程就安全了”“读过库的数据就干净了”。只要存在“读库 → 拼 SQL”这个链路,中间每个环节都得按不可信输入处理。











