唯一可靠方式是全程使用SqlParameter显式声明类型与长度绑定所有用户输入;AddWithValue禁用,表名列名等标识符须白名单或QUOTENAME()处理,任何字符串拼接均构成SQL注入漏洞。

必须用 SqlParameter 替换所有字符串拼接,.NET 6 原生支持且无兼容性问题;AddWithValue 要禁用,类型和长度必须显式声明;表名/列名等标识符不能参数化,需白名单或 QUOTENAME() 处理。
SqlCommand.CommandText 出现 +、$"" 或 string.Format 就算漏洞
只要 CommandText 里有用户输入参与拼接,哪怕只有一处,就存在 SQL 注入风险。比如:
"SELECT * FROM Users WHERE Name = '" + Request.Query["name"] + "'"$"UPDATE Logs SET Msg='{msg}' WHERE Id={id}"string.Format("DELETE FROM {0} WHERE Status='{1}'", table, status)
这些写法在 .NET 6 中依然能运行,但攻击者可传 name=admin'-- 或 id=1 OR 1=1 直接绕过。修复不是加个 Replace("'", "''"),而是彻底删除拼接逻辑,改用占位符。
Parameters.Add 必须显式指定类型和长度,别用 AddWithValue
AddWithValue 会自动推断类型,导致 SQL Server 隐式转换,可能让索引失效(尤其对主键、WHERE 条件字段)。例如:
- 传入
"admin"→ 推断为SqlDbType.VarChar,而字段是NVARCHAR(50)→ 全表扫描 - 传入长字符串 → 自动扩为
NVARCHAR(4000),浪费内存,还掩盖截断风险
正确写法示例:
cmd.Parameters.Add(new SqlParameter("@name", SqlDbType.NVarChar, 50) { Value = name ?? (object)DBNull.Value });数字型同理:new SqlParameter("@id", SqlDbType.Int) { Value = id },NULL 值必须转成 DBNull.Value,否则抛异常。
存储过程调用也得走参数化,EXEC 拼接字符串照样危险
即使封装成存储过程,如果 C# 层仍用 cmd.CommandText = "EXEC sp_GetUser @id = " + id,或存储过程内部用 EXEC(@sql) 拼接,漏洞只是转移了位置。正确方式:
- C# 层:
cmd.CommandText = "sp_GetUser"; cmd.CommandType = CommandType.StoredProcedure;,再添加@id参数 - 存储过程内:避免
EXEC(@sql),改用sp_executesql并传参,如sp_executesql N'SELECT * FROM @tbl', N'@tbl NVARCHAR(128)', @tbl不行——@tbl是标识符,不能参数化;必须用QUOTENAME(@tbl)包裹
标识符(表名、列名、排序字段)永远不能用 SqlParameter 绑定,只能靠白名单校验或 QUOTENAME() 防御。
容易被忽略的“非显性用户输入”也得参数化
除了 Request.Query、Request.Form,以下也属于用户可控输入,常被遗漏:
- 路由参数:
[FromRoute] int id—— 仍可能被构造为/user/1%20OR%201=1 - Header 值:
Request.Headers["X-User-ID"] - Cookie 值:
Request.Cookies["session_id"] - 分页参数:
page=1&size=10中的size若拼进TOP @size,必须参数化
所有 ExecuteReader、ExecuteScalar、ExecuteNonQuery 的调用点,都要检查 CommandText 是否干净。漏掉一个,整套防护就形同虚设。

















