sqlparameter是防御sql注入的唯一可靠起点,必须用parameters.add()明确指定类型传参,禁用字符串拼接、replace替换和addwithvalue隐式推断,存储过程内动态sql需用quotename和sp_executesql参数化。

用 SqlParameter 替代字符串拼接,是唯一靠谱的起点
SQL注入能发生,根本原因就是把用户输入当代码执行了。存储过程中如果用 string.Format 或 + 拼接参数,哪怕加了单引号、转义斜杠,也挡不住绕过手段。.NET 的 SqlParameter 不是“可选优化”,它是强制隔离数据与语义的边界。
- 必须通过
SqlCommand.Parameters.Add()或AddWithValue()(后者慎用类型推断)传参 - 存储过程名本身不能参数化,但所有输入参数都必须走
SqlParameter - 千万别在 SQL 字符串里写
@param然后用Replace()去替换——这和拼接没区别
cmd.CommandText = "EXEC usp_GetUserById @UserId";
cmd.Parameters.Add(new SqlParameter("@UserId", SqlDbType.Int) { Value = userId });
AddWithValue 看似方便,但类型推断常踩坑
AddWithValue 会根据传入的 C# 值自动映射 SQL 类型,比如传 null 推成 DBNull,传 "123" 推成 nvarchar(3)。问题在于:
- 存储过程定义的参数是
int,你传"123",SQL Server 可能隐式转换失败或走错执行计划 - 传空字符串
"",推成varchar(0),而存储过程期待varchar(50),可能触发截断或隐式转换警告 - 枚举值直接传,会被当成
int,但如果存储过程参数是tinyint,就可能越界
建议:
- 明确指定类型,用
Parameters.Add("@Name", SqlDbType.NVarChar, 50).Value = name - 对于
null,显式赋值DBNull.Value,别依赖推断
存储过程内部也要防注入?不,但动态 SQL 那部分得单独处理
存储过程体内的静态 SQL(如 SELECT * FROM Users WHERE Id = @Id)天然免疫注入——参数化只作用于调用层传入的值,SQL Server 解析时已把 @Id 视为纯数据。
但如果你在存储过程中拼接 SQL 字符串,比如:
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM ' + @tableName + ' WHERE Id = ' + @id
这就回到了原点。此时必须:
- 用
QUOTENAME(@tableName)处理对象名(表名、列名等) - 数值类参数仍需用
sp_executesql配合参数化,不能拼进字符串 - 绝对不要用
REPLACE或REPLICATE做“手动转义”——SQL Server 没有通用转义函数能覆盖所有注入向量
ORM(如 EF Core)调用存储过程时,参数化不是自动的
EF Core 的 FromSqlRaw 或 ExecuteSqlRaw 默认不参数化,即使你写了 @p0 占位符,若没传参数对象,它照样拼成字符串。
- 正确姿势:用
FromSqlInterpolated(带 $ 的插值语法),它底层自动转成SqlParameter - 或显式构造
DbParameter数组传给FromSqlRaw - 错误示范:
context.Users.FromSqlRaw("EXEC usp_Search @q", q)—— 这里q是裸值,不会被参数化
容易被忽略的是:EF Core 对存储过程返回结果集的映射,和参数安全无关,但若你顺手把用户输入塞进 ORDER BY 子句(比如按字段名排序),那又回到了动态 SQL 场景,得用 QUOTENAME。
参数化不是开关,是每个数据入口的强制动作;一旦漏掉一个点,整条链路就失效。










