SQL Injection

C# SQL injection

Description

SQL injection occurs when untrusted values are concatenated directly into SQL command text and interpreted as syntax rather than data. An attacker can insert conditions, additional queries, or comments to bypass authentication or read, modify, and delete unauthorized data.

Input validation and SQL escaping do not replace parameter binding. String-literal rules and character encodings vary between databases, so a helper named sanitize or escape does not itself establish that the input is safe.

Remediation

Keep the SQL structure fixed and bind every untrusted value separately from command text.

  • In ADO.NET, add provider-appropriate DbParameter values and specify types and sizes where possible.
  • In Dapper, pass values through anonymous objects or DynamicParameters.
  • In EF Core, prefer FromSql, ExecuteSql, or SqlQuery, which safely parameterize interpolated values. If a *Raw API is needed, keep SQL fixed and pass placeholder values separately.
  • SQL identifiers such as table names, column names, and sort directions cannot be bound as value parameters. Limit user choices to a small allow-list and map each to a developer-authored literal.
  • Give the database account only the permissions needed for its operations.

Examples

Before

This code inserts a request value directly into SQL text.

csharp
using System.Data;
using Dapper;
using Microsoft.AspNetCore.Mvc;

public sealed class UsersController
{
    public object Search(
        [FromQuery] string username,
        IDbConnection dapperConnection)
    {
        var sql = $"SELECT * FROM Users WHERE Username = '{username}'";
        return dapperConnection.QueryFirstOrDefault(sql);
    }
}

After

Keep SQL and values separate in ADO.NET and Dapper. AddWithValue also separates values from command text; this example specifies the type and size to reduce inference ambiguity and performance problems.

csharp
using System.Data;
using Dapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;

public sealed class UsersController
{
    public object Search(
        [FromQuery] string username,
        SqlConnection sqlConnection,
        IDbConnection dapperConnection)
    {
        const string sql =
            "SELECT * FROM Users WHERE Username = @username";

        using var command = new SqlCommand(sql, sqlConnection);
        command.Parameters
            .Add("@username", SqlDbType.NVarChar, 128)
            .Value = username;
        using var reader = command.ExecuteReader();

        return dapperConnection.QueryFirstOrDefault(
            sql,
            new { username });
    }
}

EF Core's interpolation-aware APIs turn interpolated values into parameters. Raw APIs can also receive values separately.

csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

public sealed class User
{
    public int Id { get; set; }
    public string Username { get; set; } = "";
}

public sealed class UsersController
{
    public object Search(
        [FromQuery] string username,
        DbSet<User> users)
    {
        var parameterized = users.FromSql(
            $"SELECT * FROM Users WHERE Username = {username}");

        var rawWithSeparateValue = users.FromSqlRaw(
            "SELECT * FROM Users WHERE Username = {0}",
            username);

        return new { parameterized, rawWithSeparateValue };
    }
}

When an SQL identifier must be dynamic, map the selection to a fixed literal instead of concatenating raw input.

csharp
using System.Data;
using Dapper;
using Microsoft.AspNetCore.Mvc;

public sealed class ReportsController
{
    public object List(
        [FromQuery] string sort,
        IDbConnection connection)
    {
        var orderBy = sort switch
        {
            "name" => "Name",
            "created" => "CreatedAt",
            _ => "Id"
        };

        var sql = "SELECT * FROM Reports ORDER BY " + orderBy;
        return connection.Query(sql);
    }
}

References