SQLインジェクション

C#のSQLインジェクション

説明

SQLインジェクションは、信頼できない値をSQLコマンドの文字列に直接連結し、データではなく構文として解釈させると発生します。攻撃者に条件式、追加のクエリ、コメント構文などを挿入され、認証の回避や、許可されていないデータの取得・変更・削除につながるおそれがあります。

入力検証やSQLのエスケープは、パラメーターバインディングの代わりにはなりません。文字列リテラルの規則や文字エンコーディングはデータベースによって異なるため、sanitize や escape という名前のヘルパーだけで安全性を判断しないでください。

対処方法

SQLの構造を固定し、信頼できない値はすべてコマンドの文字列と分けてバインドします。

  • ADO.NETでは、プロバイダーに適した DbParameter を追加し、可能なら型とサイズを明示します。
  • Dapperでは、匿名オブジェクトや DynamicParameters で値を渡します。
  • EF Coreでは、補間した値を安全にパラメーター化する FromSql、ExecuteSql、SqlQuery を優先します。*Raw APIが必要な場合もSQLは固定し、プレースホルダーに対応する値を別の引数で渡します。
  • テーブル名、列名、並べ替えの方向などのSQL識別子は、値パラメーターとしてバインドできません。ユーザーの選択肢を小さな許可リストに限定し、開発者が記述した固定リテラルに対応付けます。
  • データベースのアカウントには、必要な操作だけを許可する最小限の権限を与えます。

例

変更前

次のコードは、リクエストの値をSQL文字列に直接埋め込んでいます。

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);
    }
}

変更後

ADO.NETとDapperでは、SQLと値を分けます。AddWithValue も値をコマンド文字列から分離しますが、この例では型の推論による曖昧さや性能上の問題を減らすため、型とサイズを明示しています。

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の補間に対応したAPIは、補間値を自動的にパラメーターに変換します。Raw APIでも、値を別の引数として渡せます。

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 };
    }
}

動的なSQL識別子が必要な場合は、生の入力を連結せず、固定リテラルに対応付けます。

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);
    }
}

参考資料