存储过程本身不免疫sql注入,必须用sp_executesql参数化值、白名单校验标识符、显式声明类型、最小权限配置,并禁用exec(@sql)和危险扩展存储过程。

存储过程中直接拼接参数必然触发SQL注入
存储过程本身不免疫SQL注入——只要内部用了 EXEC、sp_executesql 或动态拼接字符串,且参数未被安全处理,风险就和应用层代码一样高。常见错误是开发者误以为“写在数据库里就安全”,结果在存储过程中用 +' '+@input 拼接 WHERE 条件,攻击者传入 ' OR 1=1 -- 就能绕过全部校验。
- 所有用户可控输入(如
@username、@search_term)必须视为不可信,无论来源是 Web 表单、API 还是另一个存储过程 - 避免使用
EXEC(@sql);改用sp_executesql配合参数化占位符@param - 若必须拼接表名或列名(如动态分页、多租户切换),只能通过白名单校验,例如
CASE @table_name WHEN 'users' THEN 'users' WHEN 'orders' THEN 'orders' ELSE THROW 50000, 'Invalid table', 1 END
sp_executesql 是存储过程里唯一靠谱的动态执行方式
sp_executesql 支持真正的参数化,能把变量值作为数据传入,而不是拼进 SQL 字符串。但很多人只改了调用形式,没改参数声明逻辑——比如把 @sql = N'SELECT * FROM t WHERE name = ''' + @name + '''' 改成 EXEC sp_executesql @sql, N'@name NVARCHAR(50)', @name,其实还是在拼字符串,毫无意义。
- 正确写法:先定义完整 SQL 模板(不含任何变量值),再通过参数列表传值
DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM users WHERE status = @status AND created_at > @since';EXEC sp_executesql @sql, N'@status TINYINT, @since DATETIME2', @status = 1, @since = '2025-01-01'; - 参数类型必须显式声明,且与传入值严格匹配;否则 SQL Server 可能隐式转换导致截断或失败
- 不要在模板里拼接
@table_name或@order_by——这类元数据必须走白名单或视图抽象
存储过程权限配置不当会放大注入后果
即使 SQL 注入被成功执行,如果存储过程运行账户只有 SELECT 权限,攻击者最多读数据;但如果用了 sa 或 db_owner 账户,一句 EXEC('DROP TABLE users') 就能清空整个库。很多 DBA 忘了存储过程是在数据库上下文里执行的,它的权限不是由调用方决定,而是由定义时的执行上下文(EXECUTE AS)或调用者身份决定。
- 为每个存储过程单独建最小权限角色,例如只给
usp_GetUserReport分配对users和orders表的SELECT权限 - 禁用
EXECUTE AS OWNER,除非明确需要跨架构访问;优先用EXECUTE AS 'app_reader'这类受限账户 - 禁止在生产环境启用
xp_cmdshell或sp_oacreate,这些扩展存储过程一旦被注入调用,可直接执行系统命令
ORM 调用存储过程时的隐性风险点
很多团队用 EF Core 或 MyBatis 调用存储过程,以为“用了 ORM 就安全”。但问题常出在参数绑定环节:EF Core 的 FromSqlRaw 如果传入格式化字符串,或者 MyBatis 的 ${} 语法(非 #{})直接替换变量,照样拼接 SQL。更隐蔽的是,有些存储过程本身接受 XML 或 JSON 参数,然后在内部用 OPENXML 或 JSON_VALUE 解析——如果解析后又拼进动态 SQL,风险就藏在第二层。
- EF Core 中永远用
FromSqlInterpolated而非FromSqlRaw,确保参数走 SqlParameter 机制 - MyBatis 中严格区分
#{param}(预编译)和${param}(字符串替换),后者只允许用于白名单内的固定值(如排序字段ORDER BY ${safeSortField}) - 对存储过程接收的 JSON/XML 输入,在解析前先做结构校验(如用
ISJSON()+JSON_SCHEMA_VALIDATION),拒绝非法字段名或嵌套深度超限的数据
存储过程里的 SQL 注入最难发现,因为它不在应用代码里,审计工具扫不到,日志也只记“执行了 usp_Search”,不记录实际拼出的语句。真正要命的,是那些写了十年、没人敢动、但内部用 EXEC(@sql) 处理搜索条件的老存储过程。











