存储过程不能自动防止sql注入,关键在于是否安全编写;若内部用concat、+等拼接用户输入再执行动态sql(如exec/sp_executesql),仍会中招,必须使用参数化方式(如prepare+execute配合using)或静态sql。

存储过程本身不是免疫SQL注入的“银弹”,只要内部用了动态拼接,漏洞照旧存在。
存储过程中哪些写法会触发SQL注入
很多人误以为“用了存储过程就安全了”,其实关键看存储过程里怎么处理输入参数。常见高危写法包括:
- 在存储过程中用
CONCAT()、+或字符串函数拼接用户传入的参数,再执行EXEC()/sp_executesql(SQL Server)或EXECUTE IMMEDIATE(Oracle/PostgreSQL) - 把参数直接塞进
WHERE子句的字符串里,比如SET @sql = 'SELECT * FROM users WHERE name = ''' + @name + '''' - 用参数控制表名、列名、排序字段等——这些无法用参数化占位符,但开发者常靠简单替换或白名单绕过,结果漏掉边界情况
为什么 sp_executesql 不能自动防注入
sp_executesql 本身是安全机制,但它只对“参数值”做隔离;如果 SQL 模板字符串本身是拼出来的,那模板里的逻辑就被攻击者污染了。例如:
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM ' + @table_name + ' WHERE id = @id';<br>EXEC sp_executesql @sql, N'@id INT', @id = 123;
这里 @table_name 是用户可控的,哪怕 @id 被安全绑定,整个语句仍可被诱导执行 SELECT * FROM users; DROP TABLE logs;。
真正安全的写法必须满足两个条件:
- SQL 模板字符串是硬编码或来自可信配置(不可由用户输入决定)
- 所有用户数据都通过
sp_executesql的参数列表传入,不参与字符串拼接
动态对象名(表名/列名)该怎么处理
这是存储过程中最棘手的一环——参数化查询不支持表名占位符。正确做法不是“过滤单引号”,而是:
- 严格白名单校验:只允许匹配
^[a-zA-Z_][a-zA-Z0-9_]{0,127}$这类正则,且必须查系统视图确认对象真实存在(如 SQL Server 查sys.tables) - 使用内置安全函数:SQL Server 可用
QUOTENAME(@table_name),它会自动转义并加方括号,把users; DROP TABLE x变成[users; DROP TABLE x](作为非法对象名被拒绝) - 避免运行时拼接:能用静态 SQL 就不用动态 SQL;实在要动态,优先考虑应用层路由到不同存储过程,而不是在一个过程里 if-else 拼表名
容易被忽略的“二次注入”场景
有些存储过程从数据库读出数据后,又拿去拼下一条 SQL——这叫二次注入。例如:
SELECT @old_desc = description FROM products WHERE id = @id;<br>SET @sql = 'UPDATE logs SET note = ''' + @old_desc + ''' WHERE ...';
如果 @old_desc 原本就来自用户输入且未净化,这里就复现了原始漏洞。防御要点是:
- 任何从数据库读出、后续用于拼接的数据,都视为“不可信输入”
- 不要依赖“它之前被存进去时过滤过”,因为过滤可能不一致或已过期
- 若必须拼接,一律走
QUOTENAME()(SQL Server)、quote_ident()(PostgreSQL)等专用函数
真正的难点不在语法,而在于开发时是否始终把“输入即危险”当成肌肉记忆——哪怕它来自自己的表。











