DevExpress Grid远程筛选构建WHERE条件时如何防范SQL注入
问题风险说明
你当前的实现存在极高的SQL注入风险:
- 前端拼接SQL片段的逻辑完全不可信,用户可以随意篡改前端传入的参数内容,构造恶意SQL语句
- 存储过程中直接将传入的
@SearchParam字符串拼接进动态SQL执行,没有任何过滤校验,攻击者可以通过构造恶意参数实现删表、拖库等操作 - 即便是
@Skip、@Take这类你认为是数字的参数,直接字符串拼接也存在潜在风险
可落地的防护方案
不要信任任何来自前端的输入,所有SQL语法相关的拼接、校验逻辑必须放在服务端完成,核心遵循「白名单校验+全参数化查询」两个原则:
- 废弃前端拼接SQL片段的逻辑:前端只需要传结构化的筛选规则(字段名、操作符、筛选值、逻辑连接符),绝对不要向前端传递拼好的SQL字符串
- 服务端做严格白名单校验
- 预定义所有允许被筛选的字段集合,凡是筛选条件里的字段名不在集合内的直接拦截返回错误
- 预定义所有允许使用的操作符集合(contains、notcontains、startswith、endswith、等于、大于、小于等),不在集合内的操作符直接拦截
- 预定义允许的逻辑连接符,仅支持
AND/OR,其余连接符直接拦截
- 全链路使用参数化查询,所有用户传入的筛选值、分页参数都作为SQL参数传递,绝对不要直接拼接到SQL字符串中
参考实现(C# + Dapper 层动态构建查询,废弃原有存储过程拼接逻辑)
不需要在存储过程中拼接动态SQL,直接在C#端构建安全的查询语句即可:
// 1. 定义白名单 var allowedFilterFields = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "ColumnA", "ColumnB", "ColumnC" }; var allowedOperators = new Dictionary<string, Func<object, object>>(StringComparer.OrdinalIgnoreCase) { ["contains"] = (val) => $"%{val}%", ["notcontains"] = (val) => $"%{val}%", ["startswith"] = (val) => $"{val}%", ["endswith"] = (val) => $"%{val}", ["eq"] = (val) => val, ["neq"] = (val) => val, ["gt"] = (val) => val, ["lt"] = (val) => val }; var operatorSqlMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["contains"] = "{0} LIKE @{1}", ["notcontains"] = "{0} NOT LIKE @{1}", ["startswith"] = "{0} LIKE @{1}", ["endswith"] = "{0} LIKE @{1}", ["eq"] = "{0} = @{1}", ["neq"] = "{0} <> @{1}", ["gt"] = "{0} > @{1}", ["lt"] = "{0} < @{1}" }; // 2. 解析前端传的结构化筛选条件,前端传参格式改为:[{Field:"ColumnA", Operator:"contains", Value:"ABC", Logic:"AND"}, ...] var filterConditions = 从前端请求拿到的结构化筛选列表; var whereClauseBuilder = new List<string>(); var parameters = new DynamicParameters(); var paramIndex = 0; foreach (var condition in filterConditions) { // 字段名校验 if (!allowedFilterFields.Contains(condition.Field)) throw new ArgumentException("不支持的筛选字段"); // 操作符校验 if (!allowedOperators.ContainsKey(condition.Operator)) throw new ArgumentException("不支持的筛选操作符"); // 生成参数名、处理参数值 var paramName = $"p{paramIndex++}"; var paramValue = allowedOperators[condition.Operator](condition.Value); parameters.Add(paramName, paramValue); // 拼接当前条件的SQL片段 whereClauseBuilder.Add(string.Format(operatorSqlMap[condition.Operator], condition.Field, paramName)); } // 3. 拼接最终SQL var baseSql = "SELECT ColumnA, ColumnB, ColumnC FROM dbo.TestTable"; if (whereClauseBuilder.Any()) { baseSql += " WHERE " + string.Join(" AND ", whereClauseBuilder); } // 分页参数也做参数化,不要直接拼接 baseSql += " ORDER BY ColumnA ASC OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY;"; parameters.Add("Skip", skip, DbType.Int32); parameters.Add("Take", take, DbType.Int32); // 4. 用Dapper执行查询 var result = connection.Query<TestTableModel>(baseSql, parameters);
如果你必须保留存储过程的实现,不要传入拼接好的SQL字符串,而是将结构化筛选条件作为表值参数传入存储过程,在存储过程内完成同样的白名单校验,通过sp_executesql的参数化能力传入所有筛选值,禁止直接拼接用户传入的内容到SQL语句中。
注意:不要尝试通过转义单引号、替换关键字的方式做黑名单防护,这类防护非常容易被绕过,只有白名单+全参数化是可靠的防护方式。
内容的提问来源于stack exchange,提问作者Ahad Porkar
相关产品推荐
相关产品推荐

