SQL injection is still one of the easiest vulnerabilities to introduce in an application.
Despite modern frameworks providing safer defaults, SQL Injection vulnerabilities still appear in production applications, often due to a lack of understanding of how database queries are executed.
In this article we will look at what SQL injection is, how it is exploited and how to prevent it with EF Core and Dapper.
What Is SQL Injection
SQL injection lets an attacker change the query your application sends to the database.
Consider the following example:
using var connection = connectionFactory.OpenConnection();
var order = await connection.QueryFirstOrDefaultAsync<Order>(
$"SELECT * FROM "Orders" WHERE "SerialNumber" = '{serialNumber}'");
At first glance, this may seem harmless. However, if an attacker submits the following value:
SELECT * FROM "Orders" WHERE "SerialNumber" = '' OR 1=1 --'
The OR 1=1 condition is always true, so the query can return every order instead of one lookup. The -- comments out the rest of the line.
SELECT * FROM "Orders" WHERE "SerialNumber" = 'anything'; DELETE FROM "Orders"; --
At this point, the database is no longer executing a simple lookup, it's executing arbitrary commands and the attacker can potentially read, modify or delete data.
The fix is straightforward, use parameterized queries so user input is always sent as a value, never as part of the command.
Prevent SQL Injection with EF Core
The safest default in EF Core is LINQ. The provider translates your query into parameterized SQL, so user input is not embedded in the command text:
var order = await dbContext.Orders
.FirstOrDefaultAsync(o => o.SerialNumber == serialNumber);
Even though serialNumber comes from the user, EF Core sends it as a SQL parameter:
SELECT o."Id", o."SerialNumber", ...
FROM "Orders" AS o
WHERE o."SerialNumber" = @__serialNumber_0
This means that regardless of what the user inputs, it's always treated as a value, not part of the SQL query.
When you need raw SQL, simply use placeholders with FromSqlRaw:
var orders = await dbContext.Orders
.FromSqlRaw(
"SELECT * FROM "Orders" WHERE "SerialNumber" = {0}",
serialNumber)
.ToListAsync();
NOTE: Placeholders like {0} are replaced with parameters at execution time. Do not build the SQL string with $"..." or + for values that come from the user.
For most scenarios, sticking to LINQ keeps you on the safe path without thinking about SQL syntax at all.
Prevent SQL Injection with Dapper
With Dapper, you write the SQL yourself, so parameterization is your responsibility. Use named parameters and pass values in a separate object:
const string sql = """
SELECT *
FROM "Orders"
WHERE "SerialNumber" = @SerialNumber
""";
var order = await connection.QueryFirstOrDefaultAsync<Order>(
sql,
new { SerialNumber = serialNumber });
Dapper maps @SerialNumber to the property on the anonymous object and sends it as a parameter, not as part of the SQL string.
The same pattern works for inserts, updates and stored procedures.
For more on setting up Dapper in ASP.NET Core, see my post on Dapper in ASP.NET Core.
Conclusion
SQL injection is still a real risk in .NET because the problem is how SQL is built, not which ORM you pick.
With EF Core, prefer LINQ and parameterized raw SQL. With Dapper, always use named parameters and keep user input out of the query string.
That small habit protects your data and keeps your API trustworthy as it grows.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!
