flawopen.com/sql-injection/Csharp
SQL Injection in C# (ADO.NET, Dapper, EF Core)
An interpolated $"..." string or + inside a SqlCommand, a Dapper call or FromSqlRaw lets a request parameter rewrite your SQL and return every row (CWE-89). The safe form for ADO.NET, Dapper and EF Core, and why the same $"" string is safe in FromSql but not in FromSqlRaw.[1], [2], [3], [4], [6]
Imagine a worksheet printed with blank boxes, while the submitted values travel in a sealed packet beside it. A C# FormattableString can preserve that split: EF Core FromSql receives the template and values separately, then sends the values as database parameters. Convert it to string or pass it to FromSqlRaw, and the writing is baked into the worksheet before the database reads it. ADO.NET needs SqlCommand.Parameters and Dapper needs a parameter object for the same separation. ORDER BY column names are not values, so map a request to a small switch of typed OrderBy expressions instead of inserting the name into SQL.[2], [3], [4], [8]
Core Concepts & Subsystem Terms
SqlCommand.Parameters- ADO.NET supplies each
SqlParameterseparately from the command text, so the parameter value is treated as data rather than SQL syntax. Use an explicitly typed parameter such asParameters.Add("@id", SqlDbType.Int).Value = idwhen its SQL type should be fixed.[3] Dapper parameter objects- Dapper maps the properties of an anonymous object to
@nameparameters:QueryAsync<T>(sql, new { status }). It also expands lists, soWHERE Id IN @idswithnew { ids }binds one parameter per element. None of it helps ifsqlwas concatenated.[4] FromSqlRaw vs FromSqlFromSqlRaw(string)takes a finished string; interpolating into it with$""pastes values into SQL text. EF Core 7+FromSql($"...")andFromSqlInterpolatedaccept aFormattableStringand parameterize each value. SQL identifiers cannot be parameters, so choose them from fixed trusted fragments or an allowlist.[2]FormattableString- When a
$""literal is passed to a parameter of typeFormattableString, the compiler keeps the format and the arguments separate instead of producing a string. That is what lets EF Core parameterize it — and why assigning it to astringvariable first silently loses the protection.[8], [2] Dynamic ORDER BY in LINQ- Column names cannot be parameters. In EF Core, map the requested key to a strongly typed
OrderBy(u => u.Email)with aswitchexpression, so only compiled expressions ever reach the SQL generator.[2]
Framework mappings
- CWE reference for the weakness described in this guide: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Step-by-Step Attack Flow
Request value reaches the repository
An ASP.NET Core action takes [FromQuery] string id and passes it to GetEmailAsync(). String binding does not parse the ID as a number; without explicit validation, a non-numeric string can reach the repository.[7]
Every row comes back
The condition matches every row. Through FromSqlRaw the result is the same: EF Core composes further LINQ on top of the injected SQL.[2]
Source Code: Flaw vs. Secure Implementation
// UserRepository.cs: each query below builds SQL text from request data
using Dapper;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
public class UserRepository(SqlConnection connection, AppDbContext db)
{
// ADO.NET: SqlCommand takes a string, so $"" is plain concatenation here
public async Task<string?> GetEmailAsync(string userId)
{
using var cmd = new SqlCommand($"SELECT Email FROM Users WHERE Id = {userId}", connection);
return (string?)await cmd.ExecuteScalarAsync();
}
// Dapper: concatenation, and no parameter object
public Task<IEnumerable<int>> FindOrderIdsAsync(string status) =>
connection.QueryAsync<int>("SELECT Id FROM Orders WHERE Status = '" + status + "'");
// EF Core: FromSqlRaw receives a finished string
public Task<List<User>> FindByEmailAsync(string email) =>
db.Users.FromSqlRaw($"SELECT * FROM Users WHERE Email = '{email}'").ToListAsync();
// ORDER BY column taken straight from the request
public Task<List<User>> ListAsync(string sortColumn) =>
db.Users.FromSqlRaw($"SELECT * FROM Users ORDER BY {sortColumn}").ToListAsync();
}
// UserRepository.cs: values are parameters, sorting uses typed expressions
using System.Data;
using Dapper;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
public class UserRepository(SqlConnection connection, AppDbContext db)
{
// ADO.NET: typed SqlParameter sent separately from the command text
public async Task<string?> GetEmailAsync(int userId)
{
using var cmd = new SqlCommand("SELECT Email FROM Users WHERE Id = @id", connection);
cmd.Parameters.Add("@id", SqlDbType.Int).Value = userId;
return (string?)await cmd.ExecuteScalarAsync();
}
// Dapper: anonymous object properties become @parameters
public Task<IEnumerable<int>> FindOrderIdsAsync(string status) =>
connection.QueryAsync<int>("SELECT Id FROM Orders WHERE Status = @status", new { status });
// EF Core 7+: FromSql takes a FormattableString and parameterizes {email}
public Task<List<User>> FindByEmailAsync(string email) =>
db.Users.FromSql($"SELECT * FROM Users WHERE Email = {email}").ToListAsync();
// allowlisted keys map to compiled OrderBy expressions
public Task<List<User>> ListAsync(string sortKey) => sortKey switch
{
"email" => db.Users.OrderBy(u => u.Email).ToListAsync(),
"created" => db.Users.OrderBy(u => u.CreatedAt).ToListAsync(),
_ => db.Users.OrderBy(u => u.Id).ToListAsync(),
};
}
Engineering & System Hardening Checklist
- ✓Enable CA2100 and treat it as an error (
dotnet_diagnostic.CA2100.severity = errorin.editorconfig); review every finding before suppressing it.[5] - ✓Reject
SqlCommand, Dapper,FromSqlRawandExecuteSqlRawcalls whose SQL argument is built with+,string.Format,StringBuilderor an interpolatedstringvariable.[3], [4], [2] - ✓Use
Parameters.Addwith an explicitSqlDbType, Dapper parameter objects, orFromSql/ExecuteSqlwith an inline$""literal.[3], [4], [2] - ✓Declare route and query parameters with their actual type (
int id,Guid id) and reject invalid model state before calling the repository.[ApiController]API controllers return 400 automatically for invalid model state; other controllers must handle it.[7] - ✓Build ORDER BY from a
switchexpression to typedOrderBylambdas, and connect with a login that has nodb_owneror DDL rights.[2], [3]
Sources
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- Microsoft Learn — SQL Queries in EF Core
- Microsoft Learn — Configuring parameters (ADO.NET Provider for SQL Server)
- Dapper — Parameterized queries and list support
- Microsoft Learn — CA2100: Review SQL queries for security vulnerabilities
- OWASP SQL Injection Prevention Cheat Sheet
- Microsoft Learn — Model binding in ASP.NET Core
- Microsoft Learn — FormattableString Class