唯一可靠的方式是所有用户输入都走 SqlCommand.Parameters.Add();其他任何方式如字符串拼接、AddWithValue()、拼接存储过程名或ORM映射,只要未进入Parameters集合,均无法防御SQL注入。

唯一可靠的方式是所有用户输入都走 SqlCommand.Parameters.Add(),其他任何“看起来安全”的做法——包括转义、过滤、AddWithValue()、拼接存储过程名、甚至 ORM 自动映射——只要没进 Parameters 集合,就等于没防住。
为什么字符串拼接和 AddWithValue() 不行
拼接字符串(比如 "SELECT * FROM Users WHERE name = '" + input + "'")会让单引号、分号、UNION SELECT 直接参与 SQL 解析;AddWithValue() 会隐式推断类型,空字符串 → VarChar,数字 → Decimal,可能触发索引失效或精度截断。更隐蔽的是,它对 null 的处理不一致,有时传 DBNull.Value,有时传 Object,容易崩在运行时。
常见错误现象包括:SqlException 提示“在期待条件的附近有语法错误”,其实是恶意闭合了引号;或者表面没报错,但行为异常(比如时间盲注)。
- 永远不用
string.Format()或$""拼接含用户输入的 SQL - 即使输入是数字,也别
Convert.ToInt32()后再拼字符串——攻击者可以传1; DROP TABLE Users-- - 显式指定
SqlDbType:日期用DateTime2,字符串用NVarChar,避免驱动层歧义
存储过程调用也必须参数化
很多人误以为“用了存储过程就天然安全”,其实完全错误。只要 SP 内部用了 EXEC(@sql) 或 sp_executesql 拼接用户输入,或者 C# 层手动拼接了过程名(比如 "GetUserBy" + userType),风险一点不低。
正确做法是:过程名写死在 CommandText 中,设 CommandType = CommandType.StoredProcedure,再用 Parameters.Add() 绑定参数。
- 错误示例:
cmd.CommandText = "exec SearchUsers '" + keyword + "'"—— 根本没启用参数机制 - SP 内部禁止出现
CONCAT、+连接用户输入,或QUOTENAME()后再拼接 - 如果 SP 接收排序字段等动态标识符,仍需白名单校验,不能靠参数传
动态表名、列名、ORDER BY 字段怎么处理
参数化只适用于值(value),不适用于标识符(identifier)。试图用 @sortColumn 替代 ORDER BY 字段,SQL Server 会直接报错或忽略。
可行方案只有两个:硬编码白名单,或用 QUOTENAME() + 白名单校验后拼接(仅限可信上下文)。
- 白名单示例:
var allowedSorts = new[] { "created", "updated", "name" };,检查userSort是否在其中 - 千万别写:
"SELECT * FROM Logs ORDER BY " + userSort,哪怕加了QUOTENAME()也不建议用于不可信输入 -
QUOTENAME()只防语法错误,不防逻辑绕过;它不是参数化的替代品
null 值和 DbType 必须显式处理
直接赋 Value = userName 遇到 null 会抛异常;不设 DbType 时 ADO.NET 会按值自动推断,但推断结果依赖输入内容本身,空字符串、null、数字都可能被映射成不同类型,导致隐式转换或精度丢失。
- 对可能为
null的字段:用Value = (object)userName ?? DBNull.Value - 推荐写法:
new SqlParameter("@name", SqlDbType.NVarChar) { Value = userName ?? (object)DBNull.Value } - 日期务必用
SqlDbType.DateTime2,避免 SQL Server 2005 以下版本精度截断引发意外匹配
最常被忽略的一点是:参数化查询的安全性不取决于你写了 @param,而取决于你是否把值真正塞进了 Parameters 集合。只要漏掉一个输入、用错一个占位符、或在某处悄悄拼了一次字符串,整个防护链就断了。


















