在C#开发中使用Dapper操作数据库时,很多开发者以为只要用了Dapper就自动安全了,其实不然。Dapper本身是一个轻量级ORM,它支持参数化查询,但如果你在代码里直接拼接字符串构造SQL语句,哪怕你用的是Dapper,SQL注入漏洞依然存在。真正安全的做法是:始终使用Dapper的参数化查询方式(@参数名),绝对不要把用户输入直接拼接到SQL字符串中。下面我把这个问题从头到尾讲透,包括正确写法、错误示范、内联SQL的风险以及实战中容易踩的坑。
一、Dapper参数化查询的核心原理
Dapper的参数化查询本质上是利用ADO.NET的SqlParameter机制。当你写一个带@参数的SQL语句并传入参数对象时,Dapper会把参数转换成SqlParameter,交给数据库引擎以参数化方式执行。数据库引擎会把参数当作"数据"而不是"SQL代码"来处理,这样攻击者就无法通过输入恶意字符串来改变SQL的逻辑结构。
正确的写法非常简单,看下面这段代码:
using (var connection = new SqlConnection(connectionString))
{
var sql = "SELECT * FROM Users WHERE UserName = @UserName AND Status = @Status";
var users = connection.Query<User>(sql, new { UserName = userInput, Status = 1 }).ToList();
}
这里@UserName和@Status就是参数占位符,new { UserName = userInput, Status = 1 }是匿名对象传入的参数。不管userInput里面是什么内容,哪怕是"'; DROP TABLE Users;--",数据库也只会把它当成一个普通字符串去匹配UserName字段,不会执行任何额外的SQL命令。这就是参数化查询的安全保障。
二、内联SQL拼接的致命风险
很多项目中,开发者为了"灵活"或者"方便",会直接把变量拼接到SQL字符串里。这种写法在Dapper项目中非常常见,也是SQL注入的重灾区。下面是一个典型的错误示范:
// 危险!绝对不要这样写! var sql = "SELECT * FROM Users WHERE UserName = '" + userInput + "' AND Status = 1"; var users = connection.Query<User>(sql).ToList();
如果用户输入的是:' OR '1'='1,那么最终执行的SQL就变成了:
SELECT * FROM Users WHERE UserName = '' OR '1'='1' AND Status = 1
这条语句会返回所有用户,因为'1'='1'永远为真。更危险的情况是攻击者输入:'; DELETE FROM Users;--,这会直接删除整个用户表。这不是理论上的风险,而是真实发生过无数次的安全事故。
需要特别注意的是,有些开发者会觉得"我已经对输入做了过滤和转义,应该没问题"。但手动转义在复杂场景下极易出错,比如字符编码问题、二次注入、宽字节注入等。数据库引擎提供的参数化机制是经过严格测试的安全方案,远比自己写的过滤逻辑可靠。
三、Dapper中动态SQL的安全构建方式
实际开发中,我们经常需要动态构建SQL,比如根据不同条件拼接WHERE子句。这时候很多人会想到用StringBuilder或者字符串插值来拼SQL,这就又回到了内联拼接的老路上。Dapper提供了一个很好的解决方案:使用DynamicParameters或者SqlBuilder。
先看DynamicParameters的用法:
var parameters = new DynamicParameters();
parameters.Add("@UserName", userInput);
parameters.Add("@Status", status);
parameters.Add("@MinAge", minAge);
var sql = "SELECT * FROM Users WHERE UserName = @UserName AND Status = @Status AND Age >= @MinAge";
var users = connection.Query<User>(sql, parameters).ToList();
DynamicParameters允许你在运行时动态添加参数,同时SQL语句中的占位符依然是参数化的。这样既满足了动态查询的需求,又保证了安全性。
如果你需要更复杂的动态SQL构建,可以使用Dapper.SqlBuilder这个扩展库:
var builder = new SqlBuilder();
var template = builder.AddTemplate("SELECT * FROM Users /where/");
builder.Where("UserName = @UserName", new { UserName = userInput });
builder.Where("Status = @Status", new { Status = 1 });
var users = connection.Query<User>(template.RawSql, template.Parameters).ToList();
SqlBuilder会自动帮你处理WHERE关键字的拼接逻辑(比如第一个条件用WHERE,后面的用AND),同时所有参数都是通过参数化方式传入的。这是目前Dapper生态中处理动态SQL最推荐的方式。
四、存储过程调用中的参数化注意事项
有些项目使用存储过程来封装数据库逻辑,这时候同样要注意参数传递方式。Dapper调用存储过程时,如果用CommandType.StoredProcedure,参数会自动映射为存储过程的输入参数。但如果你手动拼接了存储过程的名称或者参数,同样会有风险。
正确的存储过程调用方式:
var parameters = new DynamicParameters();
parameters.Add("@UserName", userInput);
parameters.Add("@Result", dbType: DbType.Int32, direction: ParameterDirection.Output);
connection.Execute("sp_GetUserByName", parameters, commandType: CommandType.StoredProcedure);
这里有一个容易被忽视的点:如果存储过程内部使用了动态SQL(比如EXEC(@sql)),那么即使你在调用时用了参数化,存储过程内部的动态SQL如果也是拼接的,依然存在注入风险。所以安全是一个链条,每个环节都不能掉以轻心。
五、容易被忽略的几个安全细节
第一,表名和列名不能参数化。SQL参数只能用于值(value),不能用于标识符(identifier)。如果你需要动态指定表名或列名,必须做白名单校验。比如:
// 错误:表名不能用参数
var sql = "SELECT * FROM @TableName WHERE Id = @Id";
// 正确:白名单校验表名
var allowedTables = new HashSet<string> { "Users", "Orders", "Products" };
if (!allowedTables.Contains(tableName))
throw new ArgumentException("Invalid table name");
var sql = $"SELECT * FROM {tableName} WHERE Id = @Id";
第二,LIKE查询中的通配符处理。如果用户输入直接用于LIKE,需要注意通配符%和_的转义,否则用户可以通过输入%来匹配所有记录。建议在应用层对用户输入中的通配符进行转义,或者使用参数化的方式:
var sql = "SELECT * FROM Users WHERE UserName LIKE @Pattern";
var users = connection.Query<User>(sql, new { Pattern = "%" + userInput.Replace("%", "[%]").Replace("_", "[_]") + "%" }).ToList();
第三,批量操作时的参数处理。Dapper支持批量插入和更新,这时候也要确保每个值都是参数化传入的,而不是拼接成一个大的INSERT语句。
第四,日志和调试信息中不要记录完整的SQL和参数值。虽然参数化查询本身是安全的,但如果你把拼接好的SQL(包含用户输入)记录到日志中,这些日志本身就可能成为信息泄露的渠道,也可能在后续代码审查中被误用。
六、代码审查和自动化检测建议
在团队协作中,仅靠开发者自觉是不够的。建议在代码审查流程中重点关注以下几点:凡是出现字符串拼接构造SQL的地方,必须标记为高风险;凡是使用Dapper的地方,检查是否所有用户输入都通过参数传入;可以引入静态代码分析工具,比如SonarQube、SecurityCodeScan等,它们能自动检测出SQL拼接的代码模式。
另外,定期做渗透测试也是必要的。让安全团队用专业工具对接口进行注入测试,能发现一些开发者自己意识不到的边界情况。安全不是一次性的工作,而是持续的过程。
七、总结与最佳实践清单
最后把核心要点整理成一个清单,方便大家日常对照检查:第一,永远使用Dapper的参数化查询,用@参数名加匿名对象或DynamicParameters传值;第二,绝对不要拼接用户输入到SQL字符串中,不管你觉得多"安全";第三,动态SQL用SqlBuilder或DynamicParameters构建;第四,表名列名做白名单校验,不能参数化的部分严格过滤;第五,存储过程内部的动态SQL同样要参数化;第六,建立代码审查机制和自动化安全检测流程。
Dapper是一个优秀的微ORM,它的性能和简洁性让很多项目受益。但工具本身不提供安全保证,安全取决于你怎么用它。把参数化查询当成肌肉记忆,把内联拼接当成红线,这才是真正负责任的开发方式。
