sp_executesql是防sql注入的底线,必须用参数化执行而非拼接字符串;动态对象名需quotename()处理;白名单校验+权限最小化才是完整防线。

SQL Server 存储过程中 sp_executesql 是防注入的底线
不用 EXEC(@sql) 拼接字符串,是防止 SQL 注入最直接有效的动作。SQL Server 的 sp_executesql 支持参数化执行,能确保传入值被当作数据而非代码解析。
常见错误是把用户输入拼进动态 SQL 字符串里:
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM Users WHERE name = ''' + @inputName + '''';
这样哪怕加了引号转义也容易绕过。正确做法是只拼接**结构**,把**数据**全交给参数:
-
@sql中用@paramName占位,不拼值 -
sp_executesql第二个参数声明参数类型(如N'@name NVARCHAR(50)') - 第三个及后续参数按顺序传入实际值,SQL Server 自动处理转义和类型约束
输入过滤不能替代参数化,但可作为辅助层
参数化查询已能拦截绝大多数注入,但业务上仍可能需要过滤非法字符(比如禁止用户名含 ;、--、/* 等)。注意:过滤只是增强体验或满足合规要求,不是安全主力。
典型误用是只做“黑名单”替换(如把 -- 替成空),这极易被绕过(大小写、编码、注释嵌套等)。更稳妥的做法是白名单校验:
- 对 ID 类字段,用
ISNUMERIC()或直接TRY_CAST(@input AS INT)判断是否合法数字 - 对字符串字段,用
LIKE匹配预期字符集,例如@input NOT LIKE '%[^a-zA-Z0-9_ ]%' - 长度限制必须在存储过程开头就检查,避免后续逻辑误用超长输入
不要在存储过程里调用 REPLACE 或 QUOTENAME 来“消毒”动态对象名
参数化查询无法用于表名、列名、排序字段等动态对象标识符——这是语法限制。此时若需动态构造,必须用 QUOTENAME(),而不是自己写 REPLACE 加单引号。
错误示例:
SET @sql = 'SELECT * FROM ' + REPLACE(@tableName, '''', '''''') + ' WHERE id = @id'; -- ❌ 无效且危险
正确方式:
- 用
QUOTENAME(@tableName)生成带方括号的安全标识符(如[user; DROP TABLE test]→[user; DROP TABLE test]) - 仅限于对象名、架构名等;永远不用于数据值
- 配合白名单校验更稳妥:查
sys.tables确认该表真实存在且允许访问
权限最小化比任何代码技巧都重要
即使存储过程写得再严谨,如果执行账号有 db_owner 或能跨库查询,一次成功的注入仍可能造成严重后果。真正起作用的是数据库层面的权限控制:
- 存储过程应以低权限账号(如只读角色 + 特定表写入权限)执行
- 禁用
xp_cmdshell、OPENROWSET等高危扩展(除非明确启用且审计) - 避免在存储过程中用
EXECUTE AS OWNER提权,尤其当输入来自外部时
很多团队花大量时间打磨过滤逻辑,却忘了删掉测试账号的 sysadmin 权限——这才是最常被忽略的一环。











