存储过程内用EXEC或sp_executesql拼接字符串仍危险,因SQL Server只执行最终语句;参数化仅保护值,不保护表名、列名等结构,必须白名单校验或禁用动态标识符。

存储过程里用 EXEC 或 sp_executesql 拼接字符串就危险
很多人误以为“进了存储过程就安全了”,其实只要在存储过程中动态拼接 SQL 字符串,再用 EXEC 或 sp_executesql 执行,漏洞就和应用层拼接一模一样。比如:
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM users WHERE name = ''' + @inputName + ''''; EXEC(@sql);
一旦 @inputName 是 ' OR 1=1 --,整条语句就被篡改。SQL Server 不会区分“谁写的字符串”,只认最终执行的语句结构。
-
EXEC完全不校验内容,纯文本执行,风险最高 -
sp_executesql支持参数化,但必须显式传参——如果只用它来执行拼好的字符串,照样中招 - 即使存储过程本身有权限限制,攻击者仍可能通过注入读取同库下其他表(如
sys.tables),或触发错误泄露结构
MyBatis 的 ${} 在 XML 中调用存储过程时也得小心
MyBatis 允许用 ${} 直接替换 SQL 片段,包括调用存储过程的参数部分。比如:
<select id="callProc" resultType="map"> EXEC my_proc '${tableName}' </select>这里 ${tableName} 是原样插入,没走任何预编译。如果前端传入 users; DROP TABLE logs; --,存储过程还没执行,DROP 就先跑了。
-
#{}在存储过程调用中通常不生效——因为 MyBatis 把整个EXEC ...当作一条 SQL 字符串发给数据库,#{}只对参数占位起作用,不参与存储过程内部逻辑 - 真正安全的方式是:存储过程本身不接收表名、列名这类元数据;必须动态时,用白名单校验(如只允许
'users'、'orders') - 别依赖“用户看不到存储过程代码”来免责——SQL Server 错误消息、日志、甚至某些 DBA 工具都可能暴露
sys.sql_modules内容
参数化 ≠ 存储过程自动免疫
存储过程支持参数,但参数只保护“值”,不保护“结构”。下面这段看着安全,实则仍有风险:
CREATE PROCEDURE get_user_by_id @id INT AS SELECT * FROM users WHERE id = @id AND status = 'active';
没问题。但换成这样:
CREATE PROCEDURE get_user_by_col @colName NVARCHAR(50), @val NVARCHAR(100) AS DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM users WHERE ' + @colName + ' = ''' + @val + '''; EXEC(@sql);
哪怕 @val 是参数传入,@colName 仍是拼进去的,攻击者可传 id; WAITFOR DELAY '00:00:10'; -- 实现时间盲注。
- 参数化只对
WHERE条件中的“值”有效,对表名、列名、排序字段、GROUP BY表达式等结构性内容无效 - SQL Server 的
QUOTENAME()可用于转义标识符(如列名),但它不能替代逻辑校验——QUOTENAME('users; DROP TABLE test; --')结果是[users; DROP TABLE test; --],仍会被执行 - 真正该做的是:拒绝运行时决定表/列名;必须支持时,查
sys.columns或硬编码白名单
生产环境最容易被忽略的一点:错误信息没关
开发阶段开 SET XACT_ABORT ON 或暴露详细错误是常态,但上线后若没关,一次失败的注入尝试就能返回类似 Msg 208, Level 16, State 1, Line 1 Invalid object name 'xxx' 的提示,直接暴露表是否存在、列名拼写是否正确。
- SQL Server 默认把错误发回客户端,除非应用层捕获并吞掉,或数据库配置了
user options关闭详细错误 - 即使用了参数化,错误信息泄露仍会帮攻击者绕过第一道防线——比如先用错误确认字段名,再构造布尔盲注
- 检查方式:在非调试环境下执行一个错语法的
EXEC,看返回是否含sys.objects、sys.types等系统视图名
最麻烦的不是“会不会写错”,而是“错得不够明显”——拼接逻辑藏在嵌套存储过程里、被包装成通用查询工具、或者混在日志清理脚本中,上线前没人 review,出事才翻出来。

















