FlawOpen

flawopen.com/SQL 注入/存储过程

存储过程能防止 SQL 注入吗?

参考页面
简要回答

不能——这是常见误解。只有在存储过程内部绑定参数时,它才有防护作用。如果它把输入拼接成查询字符串,再通过 EXEC 或 sp_executesql 执行,其风险与应用层字符串拼接相同。

不安全 — 将值拼接进动态 SQL
-- SQL Server T-SQL; dbo.Users.Id is NVARCHAR(50).
CREATE PROCEDURE dbo.GetUser
  @UserId NVARCHAR(50)
AS
BEGIN
  DECLARE @sql NVARCHAR(MAX) =
    N'SELECT Id FROM dbo.Users WHERE Id = N'''
      + @UserId + N'''';
  EXEC(@sql);
END;
安全 — 使用 sp_executesql 绑定参数
-- SQL Server T-SQL; same illustrative schema.
CREATE PROCEDURE dbo.GetUser
  @UserId NVARCHAR(50)
AS
BEGIN
  DECLARE @sql NVARCHAR(MAX) =
    N'SELECT Id FROM dbo.Users WHERE Id = @id';
  EXEC sys.sp_executesql
    @stmt = @sql,
    @params = N'@id NVARCHAR(50)',
    @id = @UserId;
END;

这种误解为何常见

“使用存储过程”常被当作 SQL 注入的通用修复方案。只有当过程在普通查询中把参数作为绑定值传入时,这才成立。如果它使用 EXEC 构造动态 SQL 并拼接不可信值,就会重新引入相同风险。必须使用动态 SQL 时,应通过 sp_executesql 绑定值。表名和列名不能作为值绑定;应通过严格白名单将允许选项映射到固定标识符。

常见问题

sp_executesql 是否也能安全使用?

可以。sp_executesql 支持参数:先声明参数,再单独传入值(例如 EXEC sp_executesql @sql, N'@id INT', @id = @UserId)。不要把值拼进 @sql。动态表名或列名应使用严格白名单,因为标识符不能作为绑定参数。

参考资料

另请参阅 SQL 注入(CWE-89):原理与各编程语言的防护方法 — flawopen.com TypeORM QueryBuilder 能防止 SQL 注入吗? Django ORM 与 SQL Injection:安全写法 Doctrine ORM SQL 注入:安全查询实践 EF Core FromSqlRaw:安全参数化 SQL FastAPI SQL 注入:Pydantic 能防什么,不能防什么 GORM Raw SQL:防止 Go 中的 SQL 注入