在C#项目里用Dapper做数据访问时,开发者常常图省事把查询条件和用户输入拼成完整SQL字符串。这种做法在Dapper中并不会被自动净化,数据库收到的仍是一条拼接好的指令,从而留下SQL注入缺口。强制使用匿名对象传递参数,可以让Dapper把参数值作为独立数据发送给数据库引擎,从机制上切断注入路径。

为什么拼接SQL会在Dapper中导致注入
Dapper是一个轻量级的ORM,它的核心职责是把C#对象映射成数据库命令并执行。它并不对传入的SQL字符串做语义分析或危险字符过滤。当你写出类似string.Format("select * from user where name='{0}'", input)这样的代码时,Dapper只是原样把这段字符串交给ADO.NET。如果input内容是admin' or '1'='1,最终发往数据库的指令就变成了两条逻辑被篡改的查询条件。
不少团队误以为用了Dapper就天然安全,这是典型的概念混淆。Dapper的安全边界取决于你如何传参,而不是用了哪个ORM。只要SQL文本里混入了外部输入,就绕过了参数化查询的保护。下面这段有问题的代码展示了危险写法:
string userName = Request.QueryString["name"];
string sql = "select id, name from users where name = '" + userName + "'";
using (var conn = new SqlConnection(connStr))
{
var list = conn.Query(sql).ToList();
}
上面代码里userName来自请求参数,直接与SQL拼接。攻击者传入' drop table users --就能造成破坏。这种写法在本地测试时一切正常,上线后却成为最容易被扫描利用的弱点。
匿名对象传参如何阻断注入
匿名对象在C#里用new { 属性名 = 值 }的形式创建,Dapper会把它每一个属性识别为一个参数,并通过DbCommand的Parameter集合发给数据库。数据库收到的是带占位符的指令和独立的参数值,两者在协议层分离,值不会被当作SQL语法解析。即使值里包含单引号或分号,也仅仅是字符串内容。
使用匿名对象时,SQL里用@符号声明参数名,Dapper按属性名自动匹配。下面是正确的改写方式:
string userName = Request.QueryString["name"];
string sql = "select id, name from users where name = @name";
using (var conn = new SqlConnection(connStr))
{
var list = conn.Query(sql, new { name = userName }).ToList();
}
这里@name对应匿名对象的name属性。无论userName是什么,数据库都只把它当作用户名去比较。这种方式既简洁又安全,不需要手动创建SqlParameter,也不会因为漏写引号而出错。
强制规范的落地与旧代码改造
要在团队里强制使用匿名对象,首先可以在代码评审清单中把拼接SQL列为禁止项。对于已有项目,可以写一个小工具扫描源码中Query、Execute方法调用,检查其SQL参数是否为常量加变量拼接。发现一处改一处,成本通常不高。
当查询条件动态变化时,也不要退回拼接,而是用匿名对象配合条件拼接参数对象。例如列表筛选有多个可选条件:
var sql = "select * from orders where 1=1";
var param = new DynamicParameters();
if (!string.IsNullOrEmpty(status))
{
sql += " and status = @status";
param.Add("status", status);
}
if (minPrice.HasValue)
{
sql += " and price >= @minPrice";
param.Add("minPrice", minPrice.Value);
}
using (var conn = new SqlConnection(connStr))
{
var data = conn.Query(sql, param);
}
上例用了Dapper的DynamicParameters,它本质也是参数对象,同样安全。关键是任何外部值都通过Add方法进入参数集合,而不是直接进SQL字符串。这样动态查询也能保持零注入风险。
常见误区与补充说明
有人认为对输入做转义替换单引号就足够了,这并不可靠。不同数据库对转义规则不同,且容易漏掉二次注入场景。参数化才是通用解。还有人担心匿名对象性能差,实际上Dapper对匿名对象有缓存机制,首次反射后后续调用几乎无额外开销。
最后提醒,存储过程调用、like查询也同样适用匿名对象。like的百分号应写在参数值里而不是SQL中:new { key = "%" + input + "%" },SQL写like @key即可。只要坚持所有值走参数,Dapper下的SQL注入风险就能被强制消除。