动态构建 linq 表达式本身安全,但用户输入直接拼入表达式树、fromsqlraw 插值或动态字段名未白名单校验将引发 sql 注入;orderby/select 必须预定义字段映射,fromsqlraw 须用参数化占位符,表名列名只能白名单或 quotename 转义。

动态构建 LINQ 表达式本身不会引入 SQL 注入,但若把用户输入直接塞进表达式树、拼成字符串再 Compile()、或混用 FromSqlRaw 等逃逸通道,风险就立刻出现。
OrderBy/Select 等动态字段名必须走白名单校验
LINQ 不允许运行时传入任意字符串作为属性名,比如 OrderBy(x => x.GetType().GetProperty(sortField).GetValue(x)) 看似能动态排序,实则绕过了表达式树编译,切到内存执行——这不算 SQL 注入,但会拖垮性能;更危险的是用 Expression.Property + 用户输入构造表达式,可能触发反射异常或类型绕过。
真正安全的做法是预定义合法字段映射:
- 建立字典
new Dictionary<string expression object>>>{ { "name", u => u.Name }, { "email", u => u.Email } }</string> - 用户传
sort=name时只取字典中已存在的键,不存在则抛异常或默认值 - 避免用
Expression.Parameter+Expression.Property动态拼路径,除非你严格校验sortField只能是"Name"、"Email"这几个硬编码值
FromSqlRaw 和 ExecuteSqlRaw 必须参数化,不能插值
这是最常翻车的点:很多人以为“用了 EF Core 就安全”,结果在日志查询、报表导出等场景里随手写了 context.Users.FromSqlRaw($"SELECT * FROM Users WHERE Status = '{status}'"),单引号和分号全被数据库原样执行。
正确写法只接受两种参数形式:
- 位置占位符:
context.Users.FromSqlRaw("SELECT * FROM Users WHERE Email = {0} AND Active = {1}", email, isActive) - 命名参数(SQL Server):
context.Users.FromSqlRaw("SELECT * FROM Users WHERE Email = @email", new SqlParameter("@email", email)) - 绝不能用
$""插值,哪怕只拼一个值;也别信“我过滤了单引号”,CHAR(39)或 Unicode 变体照样绕过
AsEnumerable() 后接 Where 不等于防注入,只是放弃 SQL 下推
写 context.Users.AsEnumerable().Where(x => x.Name.Contains(userInput)) 确实不会生成带参数的 SQL,但它把整张表拉到内存再筛选——数据量一大就 OOM,且没解决根本问题:如果之前已经用 FromSqlRaw 拉了原始数据,那注入风险早在 SQL 执行时就发生了。
这种写法只适合极小数据集或调试场景,生产环境应:
- 优先用
Where+ 表达式树让 EF Core 生成参数化 SQL - 若必须动态条件,用
LinqKit的PredicateBuilder组合表达式,而非字符串拼 SQL - 确认
AsEnumerable()是显式选择,不是为绕过类型检查而写的“捷径”
最易被忽略的一点:表名、列名、JOIN 条件这些标识符永远不能参数化,@tablename 在 SQL Server 里直接报错。它们只能靠白名单或 QUOTENAME() 转义,而后者必须确保输入不包含控制字符——这点在动态多租户场景里尤其致命。











