必须用 sqlcommand.parameters 替代字符串拼接sql,参数化仅保护值不保护结构;动态对象名需白名单校验;存储过程调用仍须参数化;显式指定sqlparameter类型并处理null为dbnull.value。

直接用 SqlCommand.Parameters 替换字符串拼接
所有手动拼接 "SELECT * FROM users WHERE name = '" + txtName.Text + "'" 的写法都必须立刻停用。参数化查询不是“可选优化”,而是唯一安全路径。核心动作就两步:CommandText 中用 @name 占位,再通过 Parameters.Add() 绑定值。数据库驱动会把传入的值当作纯数据处理,不再解析其中的引号、-- 或 UNION 等语法。
AddWithValue 有隐式类型风险,优先用显式构造
AddWithValue 看起来省事,但会根据传入的 .NET 类型自动推断 SQL 类型,容易引发隐式转换失败或执行计划缓存污染。比如传入空字符串 "",它可能被推为 varchar(1),而字段实际是 varchar(50),导致索引失效。更稳妥的做法是显式指定类型:
cmd.Parameters.Add(new SqlParameter("@name", SqlDbType.NVarChar) { Value = txtName.Text ?? DBNull.Value });
尤其注意 null 值要转成 DBNull.Value,否则会抛出 SqlException。
动态列名/表名不能用参数化,必须白名单校验
参数只能替代 WHERE、VALUES、ORDER BY(部分场景)中的**值**,不能替代列名、表名、排序方向或 AND/OR 逻辑结构。例如下面写法是错的:
string sql = "SELECT * FROM @table WHERE @col = @val"; // ❌ @table 和 @col 不会被识别
如果真要动态表名,必须提前定义白名单,用 switch 或字典严格映射:
var allowedTables = new Dictionary<string string> { ["users"] = "T_Users", ["orders"] = "T_Orders" };<br>string tableName = allowedTables.GetValueOrDefault(userInputTable, "T_Users");<br>string sql = $"SELECT * FROM {tableName} WHERE id = @id";</string>
任何未在白名单中声明的输入,一律拒绝或默认 fallback。
存储过程本身不免疫注入,调用方式仍需参数化
有人以为“用了存储过程就安全了”,结果在 C# 里还是拼接调用语句:"EXEC sp_GetUser '" + userName + "'" —— 这和直连 SQL 没区别。正确做法是把存储过程名当固定字符串,参数仍走 Parameters:
cmd.CommandText = "sp_GetUser";<br>cmd.CommandType = CommandType.StoredProcedure;<br>cmd.Parameters.Add(new SqlParameter("@username", SqlDbType.NVarChar) { Value = userName });
哪怕存储过程内部用了 EXEC(@sql) 动态拼接,那也是 DBA 层的责任;C# 层只要保证传入的参数不带恶意内容,就守住了第一道门。
最容易被忽略的是:参数化只保护“值”,不保护结构。一旦业务需要拼接 SQL 片段(比如多条件动态 WHERE),就得靠表达式树、QueryFilter 或 Dapper 的 SqlMapper 扩展来兜底,而不是手写字符串替换。安全边界永远在“数据与代码是否分离”这一条线上,越过去一步,漏洞就多一分。











