
Learn SQL injection prevention in C# with parameterized queries, EF Core, and stored procedures. Secure your .NET database code today with proven examples.
SQL injection remains one of the most dangerous and most common vulnerabilities in web applications today. If your C# application builds SQL queries by concatenating user input, it is almost certainly exploitable. In this guide on SQL injection prevention in C#, you will learn exactly how these attacks work, why they succeed, and how to secure your database queries completely using parameterized queries, Entity Framework Core, and stored procedures. Whether you are a beginner writing your first data-access layer or a senior engineer hardening a production system, this tutorial gives you practical, runnable examples you can apply immediately.
What Is SQL Injection and Why It Still Matters in 2026
SQL injection (SQLi) is an attack where a malicious user inserts SQL code into an input field, tricking your application into executing commands you never intended. It has topped the OWASP list of critical web risks for years, and despite being well understood, it still causes major data breaches. The reason is simple: developers keep concatenating strings to build queries.
Consider a login form. A developer might write a query like this to check credentials:
// DANGEROUS - never do this
string username = Request.Form["username"];
string password = Request.Form["password"];
string sql = "SELECT * FROM Users WHERE Username = '" + username +
"' AND Password = '" + password + "'";
using var command = new SqlCommand(sql, connection);
using var reader = command.ExecuteReader();
This looks harmless, but it is a textbook vulnerability. The user input is treated as part of the SQL command itself, not as data.
How an Attacker Exploits It
Suppose an attacker types the following into the username field:
// Attacker input for username:
' OR '1'='1' --
The query your application actually runs becomes:
SELECT * FROM Users WHERE Username = '' OR '1'='1' --' AND Password = ''
The condition '1'='1' is always true, and the -- comments out the rest of the query. The attacker is logged in without a valid password. Worse attacks can drop tables, exfiltrate every row in your database, or escalate privileges. This is why preventing SQL injection in C# is not optional — it is a fundamental security responsibility.
The Root Cause: Mixing Code and Data
Every SQL injection vulnerability stems from the same mistake: combining trusted SQL code with untrusted user data in a single string. The database engine cannot tell which parts you wrote and which parts came from an attacker. The solution to SQL injection prevention in C# is to keep code and data strictly separated so the database always treats user input as a literal value — never as executable SQL.
This principle drives every technique below. Understanding the "why" matters because it lets you spot new variations of the same flaw rather than memorizing a list of forbidden patterns.
Solution 1: Parameterized Queries in ADO.NET (The Gold Standard)
Parameterized queries — also called prepared statements — are the single most effective defense. Instead of embedding values in the SQL string, you use placeholders and pass the values separately. The database driver sends the query structure and the data over separate channels, so user input can never change the meaning of the query.
Here is the secure version of our login example using ADO.NET and SqlParameter:
using Microsoft.Data.SqlClient;
public bool ValidateUser(string username, string password)
{
const string sql =
"SELECT COUNT(1) FROM Users WHERE Username = @Username AND Password = @Password";
using var connection = new SqlConnection(_connectionString);
using var command = new SqlCommand(sql, connection);
// Values are passed as parameters, never concatenated
command.Parameters.Add("@Username", SqlDbType.NVarChar, 50).Value = username;
command.Parameters.Add("@Password", SqlDbType.NVarChar, 100).Value = password;
connection.Open();
int matches = (int)command.ExecuteScalar();
return matches == 1;
}
Now, if an attacker enters ' OR '1'='1' --, the database searches for a user whose name is literally that entire string. No user matches, and the attack fails. Notice we also specified explicit types and sizes with Parameters.Add — this is a best practice that improves performance (through query plan caching) and adds a layer of input validation.
Why Parameterized Queries Work
When you use parameters, the SQL command text is fixed and compiled before any user data is applied. The values are bound afterward as typed parameters. There is no possible input that can "break out" of a parameter and inject new SQL, because the parser has already finished. This is the strongest guarantee you can get.
Solution 2: Preventing SQL Injection in Entity Framework Core
Most modern .NET applications use Entity Framework Core (EF Core) as their ORM. The good news is that EF Core parameterizes queries automatically when you use LINQ. This code is safe by default:
// Safe - EF Core parameterizes this automatically
public async Task<User?> GetUserByEmailAsync(string email)
{
return await _context.Users
.FirstOrDefaultAsync(u => u.Email == email);
}
However, EF Core is not automatically immune. The danger appears when developers use raw SQL methods and concatenate strings into them:
// DANGEROUS - string interpolation into raw SQL string
string email = userInput;
var users = _context.Users
.FromSqlRaw("SELECT * FROM Users WHERE Email = '" + email + "'")
.ToList();
To stay secure while still using raw SQL, use FromSqlInterpolated or pass parameters explicitly. EF Core converts interpolated values into safe DbParameter objects:
// Safe - FromSqlInterpolated parameterizes the interpolated values
var users = _context.Users
.FromSqlInterpolated($"SELECT * FROM Users WHERE Email = {email}")
.ToList();
// Also safe - explicit parameters with FromSqlRaw
var users2 = _context.Users
.FromSqlRaw("SELECT * FROM Users WHERE Email = {0}", email)
.ToList();
The key rule for Entity Framework SQL injection safety: never build a SQL string with + concatenation or a plain $"..." interpolation passed to FromSqlRaw or ExecuteSqlRaw. Always let the framework create the parameters.
Solution 3: Stored Procedures Done Correctly
Stored procedures can help centralize data access and reduce your attack surface, but they are not a magic shield. A stored procedure that builds dynamic SQL internally is just as vulnerable as bad C# code. Used with parameters, however, they are secure:
public User? GetUserById(int userId)
{
using var connection = new SqlConnection(_connectionString);
using var command = new SqlCommand("sp_GetUserById", connection);
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId;
connection.Open();
using var reader = command.ExecuteReader();
return reader.Read() ? MapUser(reader) : null;
}
The stored procedure itself must avoid dynamic SQL. This T-SQL is safe because it uses the parameter directly:
CREATE PROCEDURE sp_GetUserById
@UserId INT
AS
BEGIN
SELECT Id, Username, Email FROM Users WHERE Id = @UserId;
END
Best Practices for Complete Database Security in .NET
Parameterized queries stop the injection itself, but defense in depth protects you when something slips through. Follow these best practices to secure your database queries completely.
- Always parameterize: Treat every piece of user input — including data from cookies, headers, and APIs — as untrusted. Never concatenate it into SQL.
- Use least privilege: Your application's database account should only have the permissions it needs. A read-only reporting feature should not connect with an account that can
DROP TABLE. - Validate and whitelist: Parameters cannot be used for table or column names or
ORDER BYdirections. When these must be dynamic, validate against a hard-coded whitelist rather than accepting raw input. - Hash passwords properly: Never store plaintext passwords. Use a strong algorithm like
PBKDF2,bcrypt, or ASP.NET Core Identity's built-in hashing. - Handle errors gracefully: Do not leak raw database error messages to users. Detailed SQL errors help attackers map your schema.
- Keep dependencies patched: Update
Microsoft.Data.SqlClient, EF Core, and your database engine regularly.
The Dynamic ORDER BY Pitfall
A common real-world mistake is trying to parameterize a sort column. Since you cannot parameterize identifiers, developers concatenate them — reintroducing the vulnerability. Use a whitelist instead:
// Safe way to handle a dynamic sort column
private static readonly HashSet<string> AllowedColumns =
new(StringComparer.OrdinalIgnoreCase) { "Username", "Email", "CreatedDate" };
public string BuildSortClause(string requestedColumn)
{
if (!AllowedColumns.Contains(requestedColumn))
throw new ArgumentException("Invalid sort column.");
// Value is guaranteed to be one of our known-safe strings
return $"ORDER BY {requestedColumn}";
}
Common Pitfalls That Reintroduce SQL Injection
Even teams that know about SQL injection prevention in C# get caught by these traps:
- Escaping quotes manually: Replacing
'with''is fragile and bypassable. Use parameters, not string manipulation. - Trusting client-side validation: JavaScript checks are for user experience only. Attackers bypass the browser entirely with tools like curl or Postman.
- Blocklisting keywords: Trying to strip words like
DROPorSELECTis a losing battle against encoding and creative payloads. - Mixing safe and unsafe methods: One
FromSqlRawwith concatenation anywhere in a codebase full of safe LINQ is all an attacker needs.
Conclusion: Key Takeaways
Complete SQL injection prevention in C# comes down to one core discipline: never mix executable SQL with untrusted data. Every technique in this guide enforces that separation. Here is what to remember:
- Use parameterized queries with
SqlParameterin ADO.NET — this is the gold standard. - EF Core LINQ is safe by default, but use
FromSqlInterpolatedor explicit parameters whenever you write raw SQL. - Stored procedures are secure only when they use parameters and avoid internal dynamic SQL.
- Apply defense in depth: least-privilege accounts, whitelisting for identifiers, proper password hashing, and safe error handling.
- Never rely on manual escaping, blocklists, or client-side validation.
By adopting parameterized queries everywhere and layering these best practices, you can secure your database queries completely and keep your .NET applications safe from one of the oldest and most damaging attacks on the web. Start auditing your data-access layer today — your users' data depends on it.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment