FlawOpen

flawopen.com/C# 中的 SQL Injection/EF Core FromSqlRaw

Entity Framework Core 的 FromSqlRaw 安全吗?

参考指南
简短回答

只要值与 SQL 文本分开传递,就可以安全使用。FromSqlRaw 接受原生 SQL;将不可信输入拼接或插入字符串会改变查询。应使用单独的参数实参,或将插值产生的 FormattableString 传给 FromSqlInterpolated(EF Core 7+ 也可用 FromSql)。可以把参数想成密封的值槽:数据库收到的是值,而不是新的 SQL 指令。

不安全 — 插值先变成普通字符串
// 插值会先求值并生成字符串。
var sql = $"SELECT * FROM Users WHERE Id = {id}";

// FromSqlRaw 接收的是 SQL 文本,而不是 FormattableString。
var query = context.Users.FromSqlRaw(sql);
安全 — 值始终作为参数传递
// EF Core 3–6:使用位置占位符并单独传值。
var byRawParameter = context.Users.FromSqlRaw(
    "SELECT * FROM Users WHERE Id = {0}", id);

// EF Core 3+:将插值结果传给参数化 API。
var byInterpolation = context.Users.FromSqlInterpolated(
    $"SELECT * FROM Users WHERE Id = {id}");

// EF Core 7+:FromSql 也接受 FormattableString。
var byFromSql = context.Users.FromSql(
    $"SELECT * FROM Users WHERE Id = {id}");

容易忽略的关键点

FromSqlInterpolated 接收 FormattableString。EF Core 会读取其格式和参数,并将每个插值作为独立的数据库参数发送;这不是编译时拦截。EF Core 7 新增了 FromSql,对插值字符串采用相同的安全处理。更早版本应使用 FromSqlInterpolated。当 {0} 等占位符与单独传入的值匹配时,FromSqlRaw 也可以安全使用。如果先将插值结果存入 string,参数元数据就已经丢失。

参数绑定的是值,不是列名

数据库参数表示一个值,不能表示表名、列名、SQL 运算符或排序方向。如果查询结构必须动态变化,应在应用代码中从固定的允许列表选择标识符;不要尝试参数化标识符,也不要用字符过滤代替允许列表。同时检查授权和查询成本:合法列仍可能暴露敏感字段或触发昂贵扫描。

如何检查代码库

rg -n -g '*.cs' '(FromSqlRaw|FromSqlInterpolated|FromSql)\s*\(' .
逐个检查调用所用的重载和实参类型。追踪不可信数据是否经过字符串拼接、插值后传给 FromSqlRaw,或 FormattableString 转成 string;动态标识符要单独审查。

常见问题

FromSqlRaw 接收参数时安全吗?

如果 SQL 使用占位符,并将值作为独立实参传入,则安全,例如 FromSqlRaw("... {0}", value)。不要先将值拼接或插值进 SQL 字符串。

为什么 FromSqlInterpolated 使用 C# 插值字符串是安全的?

它接收 FormattableString,可以分别保留格式和值。EF Core 会将每个值转换为数据库参数。如果先赋给 string,或传给 FromSqlRaw,这种保护就会消失。

参数能替换表名或列名吗?

不能。参数只表示值。对于动态标识符,应将用户选项映射到固定的已知标识符允许列表,并检查授权和查询成本。

参考资料

另请参阅 SQL 注入(CWE-89):原理与各编程语言的防护方法 — flawopen.com FastAPI SQL 注入:Pydantic 能防什么,不能防什么 GORM Raw SQL:防止 Go 中的 SQL 注入 转义引号足以防止 SQL Injection 吗? 使用 ORM 足以防止 SQL 注入吗? Knex.js 能防止 SQL 注入吗? MyBatis ${} 与 #{}:防止 SQL Injection