"We use an ORM, so we're safe from SQL injection" is one of the most common — and most incorrect — beliefs in .NET development. Entity Framework Core does parameterize the LINQ you write, and that covers the majority of queries safely. But EF Core also exposes raw-SQL escape hatches, and the moment a developer reaches for one and builds a string, the ORM stops protecting them. This post explains exactly where EF Core is safe, where it isn't, and how to write the raw queries you sometimes genuinely need without opening an injection hole.
Why LINQ queries are safe by default
When you write a LINQ query, EF Core translates it into a parameterized SQL command. Values become bound parameters, never inlined text:
var user = await db.Users
.Where(u => u.Email == email) // email becomes @__email_0
.FirstOrDefaultAsync();
The generated SQL is WHERE [Email] = @__email_0, with email supplied out-of-band as a parameter value. Even if email is ' OR '1'='1, it's compared as a literal string, not executed as SQL. This is why the standard advice is "prefer LINQ" — the safe path is the default path. The risk begins only when you leave LINQ for raw SQL.
The dangerous escape hatch: FromSqlRaw with concatenation
EF Core's FromSqlRaw and ExecuteSqlRaw take a SQL string as-is. If you interpolate untrusted input into that string, you've written a textbook injection:
// VULNERABLE: user input is concatenated into the command text
var rows = db.Users
.FromSqlRaw($"SELECT * FROM Users WHERE Email = '{email}'")
.ToList();
An email value of x'; DROP TABLE Users; -- is now part of the executed command. The word "Raw" in the method name is the warning label — it means "I am handing EF Core a literal string; trust it." Grep your codebase for FromSqlRaw and ExecuteSqlRaw and treat every hit as something to review.
The safe raw-SQL pattern: interpolated and parameterized APIs
EF Core provides safe siblings. FromSqlInterpolated and ExecuteSqlInterpolated (and the newer unified FromSql/ExecuteSql methods that take a FormattableString) look like string interpolation but convert every {value} into a bound parameter:
// SAFE: interpolation is captured as a FormattableString and parameterized
var rows = db.Users
.FromSql($"SELECT * FROM Users WHERE Email = {email}")
.ToList();
The distinction is subtle and dangerous: FromSqlRaw($"... {email}") runs the interpolation before EF Core sees it, producing a plain string with no parameters. FromSql($"... {email}") passes a FormattableString, so EF Core parameterizes it. Same-looking code, opposite safety. This is exactly the kind of near-identical safe/unsafe pair that automated static analysis is good at catching consistently.
If you must use FromSqlRaw — for example, because you're building SQL programmatically — pass values as explicit parameters, never in the string:
var p = new SqlParameter("@email", email);
var rows = db.Users
.FromSqlRaw("SELECT * FROM Users WHERE Email = @email", p)
.ToList();
The cases parameters can't cover: identifiers and dynamic SQL
Parameters bind values, not identifiers. You cannot parameterize a table name, a column name, or a sort direction. So dynamic ordering and dynamic column selection are where even careful developers get injected:
// VULNERABLE: column and direction come from the request
var sql = $"SELECT * FROM Products ORDER BY {sortColumn} {direction}";
The fix is an allow-list, not escaping. Map the user's requested sort to a fixed set of known-good values:
var allowed = new Dictionary<string, string> {
["price"] = "Price", ["name"] = "Name", ["date"] = "CreatedUtc"
};
if (!allowed.TryGetValue(sortColumn, out var col)) col = "CreatedUtc";
var dir = direction == "desc" ? "DESC" : "ASC";
var products = db.Products.FromSqlRaw($"SELECT * FROM Products ORDER BY {col} {dir}").ToList();
Better still, express ordering in LINQ (OrderBy/OrderByDescending) with a switch over the allow-list, so no raw SQL is involved at all. The rule: user input can select which pre-approved identifier to use, but must never become the identifier.
LIKE queries and other subtle sinks
Search features often build LIKE patterns, and naive concatenation here reintroduces injection plus wildcard abuse. Use EF.Functions.Like with a parameterized pattern, and escape the user's % and _ so they can't turn a lookup into a full-table scan:
var term = input.Replace("[", "[[]").Replace("%", "[%]").Replace("_", "[_]");
var results = db.Products
.Where(p => EF.Functions.Like(p.Name, $"%{term}%"))
.ToList();
An EF Core anti-injection checklist
| Pattern | Verdict |
|---|---|
LINQ Where/FirstOrDefault | Safe — parameterized automatically |
FromSql / FromSqlInterpolated (FormattableString) | Safe — interpolation becomes parameters |
FromSqlRaw/ExecuteSqlRaw with SqlParameter args | Safe if values are parameters, not in the string |
FromSqlRaw($"... {input}") | Vulnerable — interpolation happens first |
Dynamic ORDER BY/column from input | Vulnerable unless allow-listed |
LIKE from raw input | Escape wildcards + parameterize |
- Every
FromSqlRaw/ExecuteSqlRawreviewed for concatenation - Dynamic identifiers resolved through an allow-list
-
LIKEpatterns escape%,_, and[ - Least-privilege database account (no
DROP/ALTERat runtime)
Defense in depth matters: even a perfectly parameterized app benefits from a database login that can't drop tables, and from dynamic testing that probes your endpoints for injection the way an attacker would.
How Safeguard Helps
Safeguard catches the EF Core injection patterns that code review misses at 2 a.m. Its static analysis flags FromSqlRaw and ExecuteSqlRaw calls where interpolated or concatenated user input reaches the command text, and distinguishes them from the safe FromSql/FromSqlInterpolated forms that look nearly identical. Griffin AI then traces the data flow from the HTTP parameter to the raw-SQL sink and tells you whether the input is actually attacker-controllable in your app, so you fix the real holes instead of drowning in noise. Its dynamic application security testing confirms exploitability against the running service, and where a finding is real, the auto-fix workflow proposes the parameterized rewrite as a pull request. Teams replacing legacy SAST for this kind of taint analysis often weigh it on the Safeguard vs Checkmarx comparison.
Start free at app.safeguard.sh/register and see the EF Core scanning docs at docs.safeguard.sh.