Safeguard
Security Guides

Preventing SQL Injection in .NET with Entity Framework Core

How SQL injection still happens in Entity Framework Core apps, which EF Core APIs are safe by default, which ones aren't, and the exact patterns that keep raw SQL parameterized.

Marcus Chen
AppSec Engineer
6 min read

"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

PatternVerdict
LINQ Where/FirstOrDefaultSafe — parameterized automatically
FromSql / FromSqlInterpolated (FormattableString)Safe — interpolation becomes parameters
FromSqlRaw/ExecuteSqlRaw with SqlParameter argsSafe if values are parameters, not in the string
FromSqlRaw($"... {input}")Vulnerable — interpolation happens first
Dynamic ORDER BY/column from inputVulnerable unless allow-listed
LIKE from raw inputEscape wildcards + parameterize
  • Every FromSqlRaw/ExecuteSqlRaw reviewed for concatenation
  • Dynamic identifiers resolved through an allow-list
  • LIKE patterns escape %, _, and [
  • Least-privilege database account (no DROP/ALTER at 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.

Never miss an update

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