Safeguard
Application Security

Preventing SQL Injection with Entity Framework

EF Core parameterizes every standard LINQ query by default — the real injection risk lives in three raw-SQL escape hatches Microsoft documents but developers still misuse.

Safeguard Research Team
Research
6 min read

EF Core has shipped default query parameterization since its first stable release in 2016, and that single design decision is why there is no CVE against EF Core's LINQ pipeline for classic SQL injection. Every value passed through Where(), .Contains(), .FirstOrDefault(), or any other LINQ operator is compiled into a DbParameter object and sent to the database separately from the SQL text — not concatenated into a string. Yet SQL injection in .NET codebases hasn't disappeared; it has just relocated to three specific APIs Microsoft's own documentation calls out by name: FromSqlRaw(), ExecuteSqlRaw(), and their EF6-era predecessors, all of which accept a plain string and will happily execute whatever text you hand them. The "SQL Queries" page in the official EF Core docs contains a worked example — FromSqlRaw("... WHERE Name = '" + userInput + "'") — labeled explicitly as a SQL injection vulnerability, not a hypothetical. EF Core 7.0, released in November 2022, tried to reduce the surface area further by unifying the raw and interpolated APIs into a single FromSql() method with the same safety semantics as the interpolated form. This post walks through exactly which patterns are safe, which are not, and the identifier-injection gap that no parameterized API can close.

Why is standard LINQ safe from SQL injection by default?

Standard LINQ-to-Entities queries are safe because EF Core's query pipeline never builds SQL by string concatenation — it compiles an expression tree into a parameterized command. When you write context.Users.Where(u => u.Name == userInput), the LINQ provider translates that expression into SQL containing a placeholder like @__userInput_0, and userInput's actual value is attached to the DbCommand as a separate DbParameter. The database engine parses the SQL text once and binds the parameter value afterward, so there is no code path where attacker-controlled text can alter the query's structure — adding a '; DROP TABLE Users; -- to a search box just becomes a string value being searched for, not executable syntax. This holds true for .Contains(), .StartsWith(), comparison operators, and virtually every standard query operator EF Core translates. It's also why teams that stick entirely to LINQ and navigation properties can go years without a SQL injection finding — the vulnerability class requires deliberately opting into raw SQL.

When does FromSqlRaw() actually introduce an injection vulnerability?

FromSqlRaw() and ExecuteSqlRaw() introduce injection whenever the SQL string itself is built by concatenating or interpolating untrusted input before it reaches the method — the method executes the string as-is, with no inspection of how it was assembled. Microsoft's EF Core documentation states this directly, showing context.Blogs.FromSqlRaw("SELECT * FROM Blogs WHERE Name = '" + userInput + "'") as the canonical anti-pattern, since a value like x' OR '1'='1 closes the quoted literal early and rewrites the query's logic. The safe form of the same call uses positional placeholders passed as separate arguments — FromSqlRaw("SELECT * FROM Blogs WHERE Name = {0}", userInput) — which EF Core parameterizes correctly. But there's a documented catch: those {0}/{1} placeholders are only recognized when the format string is written inline at the call site. If the SQL string is built or stored elsewhere — a constant, a config value, a helper method's return value — and then passed into FromSqlRaw(), EF Core does not parse it for placeholders at all, silently defeating the protection developers assume they have.

What makes FromSqlInterpolated() safe even though it looks like concatenation?

FromSqlInterpolated() and ExecuteSqlInterpolated() look like naive string-building because they accept ordinary C# interpolated string syntax, but the compiler passes them a FormattableString object instead of a plain string — and EF Core reads that object's argument list separately from its format template. Given context.Blogs.FromSqlInterpolated($"SELECT * FROM Blogs WHERE Name = {userInput}"), the {userInput} slot is never rendered into literal text; EF Core extracts it as a bound parameter before the command is sent to the database, identically to how standard LINQ handles it. This is deliberately designed so the ergonomics of string interpolation don't force a tradeoff with safety. EF Core 7.0 folded this behavior into a single FromSql() overload that accepts a FormattableString, so as of EF Core 7 and later there's one recommended raw-SQL entry point for read queries rather than a raw/interpolated split — but the older FromSqlInterpolated() API remains fully supported and equally safe in current versions.

Where do parameterized APIs stop protecting you — and what has to fill the gap?

Parameterized APIs stop protecting you at identifiers — table names, column names, and ORDER BY/GROUP BY clauses — because SQL parameters can only bind to values, never to schema names, in any ADO.NET provider EF Core sits on top of. If an application needs to sort or filter by a dynamically chosen column name supplied by a user (say, a UI that lets users pick which field to sort search results by), interpolating that column name into FromSqlInterpolated() or FromSqlRaw() still produces raw, unparameterized SQL text at that position, regardless of which "safe" API is used. The only mitigation is manual allow-listing: validate the requested identifier against a fixed set of known-safe column names in C# before it ever reaches a SQL string, and reject or ignore anything that doesn't match exactly. This is a gap that exists independent of ORM choice — Dapper, ADO.NET, and raw SqlCommand usage all share it — and no version of EF Core's parameterization engine has ever attempted to close it, because doing so would require parsing and rewriting arbitrary SQL identifiers, not just binding scalar values.

Is EF.Functions.Like() safe for building search/wildcard queries?

EF.Functions.Like(), added in EF Core 2.0 in 2017, is safe because it's a LINQ method that translates directly to the database's LIKE operator and gets parameterized exactly like any other predicate — the wildcard pattern itself is just a string value bound as a parameter, not SQL text. A call like context.Products.Where(p => EF.Functions.Like(p.Name, "%" + userInput + "%")) builds the %...% wildcard pattern in C#, but that pattern travels to the database as a single parameter value; the database engine interprets % as a wildcard character within that bound value, not as SQL syntax an attacker can escape out of. The risk only reappears if a developer bypasses EF.Functions.Like() entirely and builds the whole LIKE clause — pattern included — inside a FromSqlRaw() string via concatenation, which reduces to the same anti-pattern already covered above. For any search-box or autocomplete feature, EF.Functions.Like() combined with standard LINQ is the recommended pattern precisely because it keeps the entire query, wildcards included, inside EF Core's parameterized pipeline.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.