存储过程参数类型检查仅防数字型注入,字符串型仍可拼接;sp_executesql需外置绑定才安全;表名列名等结构信息必须白名单校验,禁用EXEC()和MyBatis的${}。

存储过程参数类型检查只防数字型注入,不拦字符串拼接
存储过程里声明 @id INT 或 @status TINYINT 确实能拦住 1 OR 1=1 这类数字型注入——因为 SQL Server 在执行前会强制转成整数,1 OR 1=1 会被当作语法错误直接报错。但只要参数是字符串类型(@name NVARCHAR(50)),攻击者就能传入 ' OR 1=1 --,而类型检查完全不干预这个字符串的内容。
常见错误现象:
- 存储过程里用
EXEC('SELECT * FROM '+ @tableName),传入@tableName = 'users; DROP TABLE logs; --',DROP就在存储过程执行前跑了 - MyBatis 用
${tableName}调用存储过程,前端传users; WAITFOR DELAY '00:00:10'; --,延迟直接生效
sp_executesql 不等于自动安全,参数必须“外置绑定”
sp_executesql 是唯一能真正参数化的入口,但很多人误以为只要用了它就安全。关键在写法:参数必须定义在 SQL 字符串外部,并通过第二个参数列表显式绑定。
错误写法(仍拼接):
DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM users WHERE name = ''' + @inputName + ''''; EXEC sp_executesql @sql;
正确写法(值完全隔离):
DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM users WHERE name = @n'; EXEC sp_executesql @sql, N'@n NVARCHAR(50)', @n = @inputName;
要点:
-
@inputName的内容永远不会出现在@sql字符串里 - 表名、列名、排序方向(
ASC/DESC)这类结构信息无法参数化,必须白名单校验 -
EXEC(@sql)形式一律禁用,没有例外
MyBatis 调用存储过程时 ${} 和 #{} 的行为差异
MyBatis 把整个 EXEC my_proc ... 当作一条 SQL 字符串发给数据库,#{} 只对语句内部占位符起作用,对存储过程调用本身无效。所以 ${} 在这里不是“方便”,而是高危开关。
典型风险场景:
-
<select id="getByTable" resultType="map">EXEC get_data '${table}'</select>——${table}原样插入,无任何拦截 - 即使存储过程内部用了
sp_executesql,外部${}已经让恶意语句提前执行 - 解决办法只有两个:改用
#{}传参(仅适用于过程内参数),或上层代码做白名单过滤(如只允许'users'、'orders')
白名单校验比转义更可靠,尤其对标识符
试图用 REPLACE(@col, '''', '''''') 或 QUOTENAME() 处理列名,容易漏掉 Unicode 绕过、注释截断等变体。SQL Server 的 QUOTENAME() 只适合生成带括号的标识符(如 [user.name]),但不能替代业务逻辑校验。
推荐做法:
- 用
CASE WHEN @col IN ('name', 'email', 'created_at') THEN @col ELSE THROW 50000, 'Invalid column', 1 END - 把合法列名硬编码进存储过程,拒绝一切未明确定义的输入
- 对动态表名,优先考虑视图或固定查询路径,而非开放
FROM @table
最常被忽略的一点:错误消息本身可能泄露元数据。哪怕存储过程加了白名单,一旦触发 THROW,返回的错误文本若没被上层吞掉,就等于告诉攻击者哪些值是合法的。

















