SQL injection (SQLi) is one of the most common and dangerous security vulnerabilities in web applications. Attackers exploit improperly handled SQL queries to access, modify, or delete sensitive data. In ASP.NET Core APIs, even experienced developers can inadvertently introduce SQL injection if proper precautions are not taken.

This article explains what SQL injection is, how it happens in ASP.NET Core APIs, and how to prevent it using best practices, safe coding techniques, and modern libraries.

Table of Contents

  1. What is SQL Injection?

  2. How SQL Injection Happens in ASP.NET Core

  3. Risk Assessment and Common Scenarios

  4. Using Parameterized Queries

  5. Using Entity Framework Core Safely

  6. Stored Procedures

  7. ORM-Specific Protections

  8. Validating and Sanitizing Inputs

  9. Avoiding Dynamic SQL

  10. Logging and Monitoring

  11. Security Best Practices

  12. Conclusion

1. What is SQL Injection?

SQL injection is a vulnerability where an attacker can manipulate a SQL query by inserting malicious input. This can allow the attacker to:

For example, consider a naive query:

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

If username or password contains malicious SQL code, the query could be manipulated to bypass authentication or retrieve all user data.

2. How SQL Injection Happens in ASP.NET Core

In ASP.NET Core APIs, SQL injection typically occurs when developers:

Example of Vulnerable API Endpoint

[HttpGet("get-user")]
public async Task<IActionResult> GetUser(string username)
{
    var query = $"SELECT * FROM Users WHERE Username = '{username}'";
    using (var command = new SqlCommand(query, _sqlConnection))
    {
        var reader = await command.ExecuteReaderAsync();
        // Process reader...
    }
    return Ok();
}

If username = "admin' OR 1=1 --", the query becomes:

SELECT * FROM Users WHERE Username = 'admin' OR 1=1 --'

This returns all users — classic SQL injection.

3. Risk Assessment and Common Scenarios

SQL injection risks are higher when:

Common scenarios:

  1. Authentication bypass

  2. Data exfiltration

  3. Mass updates/deletions

  4. Privilege escalation

4. Using Parameterized Queries

The first and most effective defense is parameterized queries, which ensure that user input is treated as data, not SQL code.

Example with ADO.NET

[HttpGet("get-user")]
public async Task<IActionResult> GetUser(string username)
{
    string query = "SELECT * FROM Users WHERE Username = @username";

    using (var command = new SqlCommand(query, _sqlConnection))
    {
        command.Parameters.AddWithValue("@username", username);
        var reader = await command.ExecuteReaderAsync();
        // Process reader...
    }

    return Ok();
}

Benefits:

5. Using Entity Framework Core Safely

Entity Framework Core (EF Core) abstracts SQL queries and reduces the risk of SQL injection if used correctly.

Safe Usage

[HttpGet("get-user-ef")]
public async Task<IActionResult> GetUserEF(string username)
{
    var user = await _dbContext.Users
        .Where(u => u.Username == username)
        .FirstOrDefaultAsync();

    return Ok(user);
}

EF Core parameterizes queries automatically, so manual string concatenation is not needed.

Unsafe Usage in EF Core

var users = await _dbContext.Users
    .FromSqlRaw($"SELECT * FROM Users WHERE Username = '{username}'")
    .ToListAsync();

FromSqlRaw with string interpolation is vulnerable. Always use parameters:

var users = await _dbContext.Users
    .FromSqlRaw("SELECT * FROM Users WHERE Username = @username", 
        new SqlParameter("@username", username))
    .ToListAsync();

6. Stored Procedures

Stored procedures, if written properly, can prevent SQL injection. However, they must avoid dynamic SQL inside the procedure.

Example

CREATE PROCEDURE GetUserByUsername
    @Username NVARCHAR(50)
AS
BEGIN
    SELECT * FROM Users WHERE Username = @Username
END

Call in ASP.NET Core:

var users = await _dbContext.Users
    .FromSqlInterpolated($"EXEC GetUserByUsername {username}")
    .ToListAsync();

Warning: Avoid concatenating strings inside the stored procedure.

7. ORM-Specific Protections

var users = await _dbConnection.QueryAsync<User>(
    "SELECT * FROM Users WHERE Username = @Username",
    new { Username = username });

8. Validating and Sanitizing Inputs

While parameterization is primary, input validation adds an extra layer of security:

Example DTO

public class LoginRequest
{
    [Required]
    [StringLength(50)]
    [RegularExpression("^[a-zA-Z0-9_.-]+$")]
    public string Username { get; set; }

    [Required]
    [StringLength(100)]
    public string Password { get; set; }
}

9. Avoiding Dynamic SQL

Dynamic SQL is the main source of injection vulnerabilities. Instead of building queries at runtime:

Bad Example

string query = $"SELECT * FROM Users WHERE {filterColumn} = '{filterValue}'";

Good Example

var validColumns = new[] { "Username", "Email", "Role" };
if (!validColumns.Contains(filterColumn))
    throw new ArgumentException("Invalid filter column");

var parameter = new SqlParameter("@value", filterValue);
var query = $"SELECT * FROM Users WHERE {filterColumn} = @value";

10. Logging and Monitoring

Detecting potential injection attempts is crucial:

11. Security Best Practices

  1. Principle of Least Privilege: Use database users with minimal permissions.

  2. Avoid Admin Accounts: Never connect API using sa or admin accounts.

  3. Use ORM Where Possible: Reduces manual query construction.

  4. Parameterize Everything: Never concatenate strings in SQL.

  5. Validate Inputs: Ensure user data conforms to expected formats.

  6. Keep Libraries Updated: EF Core, Dapper, and SQL Server drivers often fix vulnerabilities.

  7. Penetration Testing: Test APIs with tools like SQLMap or custom scripts.

Conclusion

Preventing SQL injection in ASP.NET Core APIs is essential for production-grade applications. Senior developers should focus on:

By following these best practices, you can significantly reduce the risk of SQL injection and make your APIs secure, maintainable, and production-ready.

Key Takeaways