存储过程本身不防SQL注入,动态拼接用户输入仍存在风险;必须用sp_executesql配合参数化查询,禁止字符串拼接,标识符需白名单校验+QUOTENAME()。

存储过程里用了EXEC或sp_executesql拼接字符串,就等于开了后门
很多人误以为“用了存储过程=自动防御SQL注入”,其实完全不是。存储过程本身只是数据库端的封装函数,它不改变SQL解析逻辑。一旦内部用EXEC或sp_executesql动态拼接用户输入,漏洞立刻复现。
典型错误写法:
CREATE PROCEDURE GetUserByName
@name NVARCHAR(50)
AS
BEGIN
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM users WHERE username = ''' + @name + '''';
EXEC(@sql);
END
攻击者传入@name = 'admin'' OR 1=1 --',拼出的语句就是:SELECT * FROM users WHERE username = 'admin' OR 1=1 --',和前端拼接无异。
- 只要字符串拼接发生在SQL执行前(无论在应用层还是数据库层),就存在注入风险
-
sp_executesql本身不安全——只有配合参数化才安全;单独用它拼接仍是高危操作 - SQL Server中
QUOTENAME()只适合表名/列名这类标识符,不能用于值过滤
正确用sp_executesql必须带参数占位符,不能只靠REPLACE或QUOTENAME
安全写法的核心是:把用户输入作为参数传入,而不是拼进SQL字符串。哪怕在存储过程中,也要坚持“模板+参数”分离原则。
正确示例:
CREATE PROCEDURE GetUserByName
@name NVARCHAR(50)
AS
BEGIN
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM users WHERE username = @n';
EXEC sp_executesql @sql, N'@n NVARCHAR(50)', @n = @name;
END
- 占位符
@n必须出现在SQL模板里,且sp_executesql第二个参数明确定义类型和长度 - 第三个参数才是真正的值绑定,此时SQL Server在解析阶段已锁定语法结构,@name只当数据处理
- 不要试图用
REPLACE(@name, '''', '''''')做转义——绕过方式太多,比如宽字节、嵌套引号、Unicode编码
动态对象名(表名、列名)无法参数化,必须严格白名单校验
参数化查询只适用于值(WHERE条件、INSERT字段值等),对表名、列名、排序字段这些标识符无效。这时候拼接不可避免,但必须收口。
- 禁止直接使用
@tablename拼进FROM子句 - 应预设合法范围,例如:
IF @sort_col NOT IN ('username', 'email', 'created_at') THROW 50000, 'Invalid column', 1; - 用
QUOTENAME(@col)包裹后再拼接,防止]; DROP TABLE users;--这类注入 - 避免在存储过程中接受任意
@db_name或@schema——跨库/跨schema调用本身就是权限滥用信号
ORM或应用层调用存储过程时,仍可能绕过防护
即使存储过程本身写得再安全,如果上层代码用字符串拼接来构造EXEC proc_name 'xxx',或者把用户输入硬塞进SqlParameter却没设SqlDbType和Size,防护就形同虚设。
- .NET中用
SqlCommand调用存储过程时,必须用AddWithValue或显式Add,且类型要匹配(如NVARCHAR对应SqlDbType.NVarChar) - Python的
pyodbc中,cursor.execute("exec GetUserByName ?", username)才是安全的;cursor.execute(f"exec GetUserByName '{username}'")直接回退到原始漏洞 - 特别注意ORM的
raw()或execute()方法——它们绕过所有ORM层的参数化保护,等同于裸写SQL
真正容易被忽略的是:存储过程的安全性不取决于它“在数据库里”,而取决于它“怎么处理输入”。哪怕封装得再深,只要有一处+、CONCAT或未校验的EXEC,整条链路就断了。

















