string.Format拼SQL必然导致SQL注入,因用户输入会直接混入SQL结构;必须用参数化查询(如SqlCommand.Parameters.Add)隔离值与结构,并对动态表名/列名等采用白名单校验。

为什么 string.Format 拼 SQL 一定会出事
因为数据库不会区分“你是想查用户”,还是“你想删表”。只要字符串里混了用户输入,WHERE name = '{input}' 这种写法,攻击者塞个 ' OR '1'='1 就能绕过条件;塞个 '; DROP TABLE users; -- 就直接执行非法命令。这不是“可能被利用”,是只要输入可控,就必然可利用。
常见错误现象:SqlException 报错信息里出现意外的 OR、UNION 或多出的引号;查询返回本不该看到的数据;甚至日志里发现 SELECT * FROM users WHERE id = 1; DELETE FROM orders; 这种明显拼接痕迹。
- 永远别用
string.Format、$""插值、+拼接构造 SQL 条件或表名/列名 - 连“管理员固定查 status=1”这种看似安全的硬编码,一旦未来加个动态筛选入口,就会立刻变成漏洞点
- ORM 的
.Where(x => x.Name == input)是安全的,但.Where($"name = '{input}'")(EF Core 早期某些扩展)照样危险
用 SqlCommand + Parameters.Add() 是最稳的写法
SQL Server 的参数化查询本质是把值和结构分开:命令文本是纯模板,参数是独立数据包,驱动层确保它们不混合解析。哪怕你传入 Robert'; DROP TABLE Students; --,数据库也只当它是字符串值,绝不会当成语句执行。
使用场景:所有带用户输入的 SELECT、INSERT、UPDATE、DELETE,包括分页参数、模糊搜索关键词、状态筛选下拉值。
- 必须用
cmd.Parameters.Add("@name", SqlDbType.NVarChar).Value = userInput,而不是AddWithValue—— 后者会猜类型,遇到空字符串可能推成varchar(0)导致索引失效 - 参数名必须以
@开头,且不能和变量名重复(比如别写cmd.Parameters.Add("@id", id),id是变量,这里要传值) - 批量操作时,每个参数单独
Add,不要试图用循环拼@p0、@p1—— 那又回到手动管理的脆弱路径
var cmd = new SqlCommand("SELECT * FROM users WHERE status = @status AND name LIKE @pattern", conn);
cmd.Parameters.Add("@status", SqlDbType.Int).Value = status;
cmd.Parameters.Add("@pattern", SqlDbType.NVarChar).Value = $"%{search}%";
IN 列表和动态列名没法直接参数化,得换思路
IN (@p1, @p2, @p3) 看似参数化,但实际不支持变长列表——你不能在运行时决定加几个 @p。而表名、列名、排序字段这些语法成分,数据库根本不允许用参数占位,否则会报 Incorrect syntax near '@table'。
性能影响:硬拼 IN 列表会导致每次不同长度都生成新执行计划,缓存利用率暴跌;列名拼接则完全失去参数化保护。
-
IN场景改用临时表或表值参数(DataTable+SqlDbType.Structured),或服务端拆成多次单值查询(小数据量可行) - 动态列名/表名必须白名单校验:
if (!new[] { "created_at", "updated_at", "status" }.Contains(sortField)) throw ...,绝不走正则替换或字符串包含判断 - 排序方向(
ASC/DESC)同样不能参数化,必须白名单:orderDirection = orderDirection.ToUpper() == "DESC" ? "DESC" : "ASC"
ORM 里也得盯紧底层 SQL 是否真参数化
EF Core 的 LINQ 查询默认安全,但一写 FromSqlRaw 或 ExecuteSqlRaw 就立刻退化回手写 SQL 风险区。Dapper 的 Query<T>(sql, param) 安全,但 Query<T>(sql + $"'{input}'") 就等于裸奔。
容易踩的坑:context.Users.FromSqlRaw("SELECT * FROM users WHERE name = {0}", userInput) —— 这个 {0} 不是参数占位符,是 string.Format 语法,照样注入;EF Core 5+ 才支持 FromSqlInterpolated,写成 $"SELECT * FROM users WHERE name = {userInput}" 才真正参数化。
- Dapper 中确保
param是匿名对象或IDictionary,不是拼好的字符串 - EF Core 用
FromSqlInterpolated时,变量必须用{xxx}包裹,不能写成"... = " + xxx - 任何 ORM 的“原生 SQL”执行方法,调用前先用 SQL Server Profiler 或 EF 日志确认生成的最终语句是否含参数(如
exec sp_executesql N'SELECT...', N'@p0 nvarchar(4000)', @p0=N'abc')
参数化不是加个 @ 就完事,关键在值与结构的物理隔离。白名单校验、类型显式声明、ORM 原生 SQL 的调用姿势,每一步漏掉都可能让防护形同虚设。

















