How to prevent SQL injection in C#

SQL injection remains a serious threat in modern web apps. This guide shows how C# developers can prevent injection with secure coding practices and verify security with a DAST-first approach that identifies exploitable vulnerabilities before the attackers can find them.

How to prevent SQL injection in C#

SQL injection remains one of the most dangerous security risks for web applications. In C# as in any other language, developers need to understand such injection vulnerabilities and prevent them through secure coding practices. On top of that, modern application security demands a proactive, DAST-first approach that focuses on real, exploitable vulnerabilities—not theoretical risks in static code. 

What is SQL injection?

First, let’s get clear on definitions. What are we defending against? SQL injection (SQLi) happens when attackers inject malicious SQL code into an application’s queries, exploiting improper handling of user input. This can allow unauthorized access, data manipulation, or even complete database compromise.

An example of vulnerable C# code might be:

string query = "SELECT * FROM Users WHERE Username = '" + username + "' AND Password = '" + password + "'";

With this authentication logic, the application expects the database to return a non-empty result set if the user name and password combination exists in the Users table. If a malicious user is able to input admin' OR '1'='1 into a user name field or parameter, the query becomes:

SELECT * FROM Users WHERE Username = 'admin' OR '1'='1' AND Password = ''

If executed by the database, this query will always return results (because the 1=1 condition is always true), in effect bypassing authentication in the app.

Best practices for preventing SQL injection in C#

Defending against SQL injection is ultimately about not giving attackers a chance to insert or modify database queries built and sent by the application.

Use parameterized queries

Parameterized queries separate SQL logic from user data to prevent input from being interpreted as executable SQL. User-controlled inputs are automatically sanitized and encoded by the runtime to make sure they can be safely executed as part of a query. Here’s a simplified example of securely checking the username and password in C#:

using (SqlConnection connection = new SqlConnection(connectionString))
{
    string query = "SELECT * FROM Users WHERE Username = @Username AND Password = @Password";
    SqlCommand command = new SqlCommand(query, connection);
    command.Parameters.AddWithValue("@Username", username);
    command.Parameters.AddWithValue("@Password", password);
    connection.Open();
    SqlDataReader reader = command.ExecuteReader();
}

Note that in actual production code, you should be working with password hashes, never the plaintext password as in this simplified example. 

To add some input validation at this stage, you can use Parameters.Add() instead of Parameters.AddWithValue() and specify the expected data type and length when creating the new SqlParameter object:

command.Parameters.Add(new SqlParameter("@Username", SqlDbType.NVarChar, 50) { Value = username });

Use ORM frameworks whenever possible

While they are not specifically designed for security, object-relational mappers (ORMs) like Entity Framework can help prevent injections because they handle query parameterization automatically:

var user = context.Users.FirstOrDefault(u => u.Username == username && u.Password == password);

Using ORM correctly can not only reduce the risk of SQL injection but makes it easier to write more secure code overall while also making the code more robust by abstracting away the database interactions.

Implement defense-in-depth

On top of these secure coding best practices, follow a defense-in-depth approach to further limit the risk of injections. Note that none of these alone will prevent injection, so they should always be applied in combination:

  • Use parameterized stored procedures: Stored procedures can be a powerful way to reduce injection risk by defining and storing the query in the database itself, separately from input data processing. To be secure, though, they still need to be properly parameterized—a dynamically generated stored procedure can be just as vulnerable as a query built from concatenated strings.
  • Validate inputs: Enforce relevant format constraints for inputs to narrow down injection possibilities (for example, disallow string inputs where only an integer is expected).
  • Sanitize inputs: Check inputs against expected values where possible and filter out any obvious special characters, always keeping in mind that any filter can be bypassed and shouldn’t be trusted as your only defense. 
  • Apply the principle of least privilege: Use restricted database accounts to limit the impact of any successful attack. This applies both to the database user account accessed by the application and to the OS user of the database process on the server.
  • Regularly scan for exploitable SQLi: Dynamic application security testing (DAST) solutions like Invicti can scan live applications for exploitable vulnerabilities in a continuous process, verifying the real-world impact of potential issues.

Why manual sanitization and filtering are not enough to stop injection

To prevent injection, it’s never enough to simply strip any SQL-specific special characters from inputs, as in the following example:

public static string SanitizeSqlString(string input)
{
    return input?.Replace("'", "").Replace("\"", "").Replace(";", "");
}

This approach is brittle and easily bypassed (see the Invicti SQL injection cheat sheet for many examples of attack payloads that would be unaffected), not to mention the risk of breaking valid inputs in edge cases.

Likewise, regex-based validation only filters input patterns, and while it can improve input quality and the user experience (e.g., by catching some typing errors before submission), it won’t prevent query execution and shouldn’t be treated as a standalone security measure:

if (Regex.IsMatch(username, @"^[a-zA-Z0-9]+$")) { /* not sufficient */ }

Ideally, you should use built-in sanitization and validation features provided by the language or framework as a routine secure coding practice. Treat them as one part of a defense-in-depth approach that also includes using DAST to probe the running application for exploitable SQLi vulnerabilities in both staging and production-identical environments.

Code examples: How to secure C# MVC and LINQ from SQL injection

In ASP.NET MVC, use model validation for format control and rely on parameterized queries or ORM-based data access to minimize the risk of SQLi:

[HttpPost]
public ActionResult Login(string username, string password)
{
    if (ModelState.IsValid)
    {
        using (SqlConnection connection = new SqlConnection(connectionString))
        {
            string query = "SELECT * FROM Users WHERE Username = @Username AND Password = @Password";
            SqlCommand command = new SqlCommand(query, connection);
            command.Parameters.AddWithValue("@Username", username);
            command.Parameters.AddWithValue("@Password", password);
            connection.Open();
            SqlDataReader reader = command.ExecuteReader();
        }
    }
    return View();
}

When using LINQ, you don’t need to directly manipulate SQL at all to define and run a query:

var users = context.Users.Where(u => u.Username == username);

Combined with an ORM like Entity Framework, this is safe due to automatic parameterization (provided you avoid deliberately writing raw SQL).

Beyond injection: Staying proactive with DAST

SQL injection is just one of many types of application vulnerabilities, and C# is only one of many technologies used to build web apps. Regardless of your tech stack, systematic and automated vulnerability scanning is a must. Invicti DAST provides tech-agnostic proof-based scanning to accurately identify vulnerabilities and verify them with concrete evidence. This avoids false positives and enables faster, more confident remediation.

Invicti’s DAST-first approach to AppSec means:

  • Scanning running applications, not just source code
  • Prioritizing exploitable vulnerabilities
  • Avoiding alert fatigue from SAST and static SCA noise
  • Delivering only validated and actionable results to developers

Preventing SQL injection in C# starts with parameterized queries and ORM frameworks but doesn’t end there. To truly protect your applications and improve security in the long run, you need continuous validation from a DAST-first security program. Real security is about fixing what attackers can actually exploit—not chasing shadows in static code.

About the Author

Priyank Savla