存储过程内使用EXEC拼接动态SQL等于裸奔,因SQL Server不自动参数化,表名列名等无法参数化,必须用白名单校验;二次注入和权限最小化同样关键。

存储过程里用 EXEC 拼字符串等于裸奔
哪怕整个调用链都走存储过程,只要内部出现 EXEC(@sql) 或 EXEC sp_executesql @sql 且 @sql 是拼出来的,就和直接拼接用户输入没区别。SQL Server 不会因为你写了 CREATE PROCEDURE 就自动帮你过滤或参数化。
典型错误模式:
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM users WHERE id = ' + CAST(@id AS VARCHAR(10));<br>EXEC(@sql);
这里 @id 是 INT 类型,但 CAST 后转成字符串再拼进 SQL,攻击者根本不需要传入恶意字符串——只要上游调用时把 @id 设为 1 OR 1=1,SQL Server 在隐式转换阶段就会报错或截断,而真正危险的是:如果 @id 来自应用层未校验的字符串参数(比如 @id NVARCHAR(50)),那拼进去的就是纯代码。
-
EXEC(@sql)完全不支持参数绑定,必须杜绝 -
sp_executesql的第二个参数(类型声明)必须显式写出完整类型,例如N'@status TINYINT',不能只写N'@status INT'而忽略长度(对字符串尤其关键) - 一旦用了动态拼接,所有参与拼接的变量都得是字符串类型,否则像
'WHERE id = ' + @id(@id是 INT)会直接报错,这不是安全机制,只是类型不匹配的编译失败
表名、列名、ORDER BY 字段无法参数化,白名单是唯一出路
SQL Server 语法限制:@table_name、@order_col 这类对象名不能作为参数占位符。硬拼就是高危操作,没有例外。
常见错误写法:
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM ' + @table_name + ' ORDER BY ' + @order_col;<br>EXEC sp_executesql @sql;
这段代码无论怎么加 sp_executesql 都拦不住注入——因为 @table_name 和 @order_col 是直接嵌入语法结构的。
- 必须提前定义合法值集合,比如用临时表
#allowed_tables存允许的表名,再用IF NOT EXISTS (SELECT 1 FROM #allowed_tables WHERE name = @table_name) RAISERROR(...)校验 - 不要依赖
REPLACE(@input, '''', '''''')或正则删单引号——绕过方式太多,且容易漏掉括号、方括号、点号等合法但危险的字符 - 排序字段若来自用户,优先考虑前端固定选项(如“按时间降序”“按姓名升序”),后端只做映射,不拼字段名
二次注入风险在存储过程中照样存在
所谓“二次注入”,是指恶意数据第一次存入数据库时看似正常(比如昵称填了 admin'; DROP TABLE users--),之后某次存储过程读出该字段、再拼进新 SQL 执行,才真正触发攻击。存储过程不是免疫区。
典型场景:
- 用户注册时昵称被存入
users.nickname字段,无过滤 - 后台报表存储过程读取
SELECT nickname FROM users WHERE active = 1,然后拼成'UPDATE logs SET remark = ''' + @nickname + ''' WHERE ...' - 这时
@nickname已含单引号和 SQL 片段,拼完就执行
防御要点:
- 任何从数据库读出、再用于构建动态 SQL 的值,都要当作“不可信输入”重新校验,不能因为“它之前存过”就放行
- 避免在存储过程中对读出的字符串做
REPLACE或QUOTENAME后直接拼接——QUOTENAME只适用于对象名,不适用于普通字符串值 - 对字符串值的使用,应坚持参数化路径:读出后作为
sp_executesql的参数传入,而不是拼进 SQL 字符串
权限最小化不是可选项,是存储过程生效的前提
即使所有存储过程都写得滴水不漏,如果调用账户有 db_owner 或对表的 SELECT/INSERT 权限,攻击者就能绕过过程直接查表、删表。存储过程的安全模型依赖所有权链(ownership chaining),前提是基础表权限被收回。
- 执行
REVOKE SELECT ON users FROM public,再GRANT EXECUTE ON Sp_GetUser TO app_user - 确认存储过程
DEFINER(SQL Server 中是EXECUTE AS)上下文与表所有者一致,否则所有权链断裂,权限检查会回退到调用者 - 禁用
guest用户,防止匿名连接拿到默认权限
最易被忽略的一点:很多团队以为“用了存储过程=防注入”,却忘了撤掉表权限。这时候哪怕过程本身安全,攻击者也能用 EXEC('SELECT * FROM sys.tables') 这类语句探测结构,为后续攻击铺路。

















