Skip to main content

クエリパラメータ化の基礎

著者
Headshot of Mary Gathoni

Mary Gathoni

blog feature toolkit

2023年10月24日

0 分で読めます

Webアプリケーションがユーザー提供のデータを使用する場合、SQLインジェクション攻撃のリスクが生じる可能性があります。SQLインジェクションとは、ユーザー入力を操作してSQL文を改変する手法です。具体的には、攻撃者が悪意のあるSQLコードをデータベースに送信し、セキュリティ対策を回避してデータへの不正アクセスを試みます。その結果は深刻で、機密情報の漏えいからシステムの完全な喪失に至ることもあります。  

SQLインジェクション攻撃を防ぐ方法はいくつかありますが、重要な手法の1つがSQLクエリのパラメータ化です。これは、プレースホルダー(パラメーター)を使ってSQLコードとユーザー入力値を分離し、入力をSQLコードではなくデータとして扱う方法です。重要なのは、パラメーター化クエリと組み合わせてプリペアドステートメントを使うことで、大きな効果が得られる点です。

セキュリティをさらに強化するには、パラメーター化されたストアドプロシージャを使用できます。ストアドプロシージャとは、データベースに保存されたプリコンパイル済みSQL文です。ストアドプロシージャの実行権限を付与しながら、基盤となるテーブルへの直接アクセスを制限できます。プロシージャを使う際は、アプリケーションから必要なパラメーターを渡します。

この記事では、SQLクエリのパラメータ化やストアドプロシージャを活用してインジェクション攻撃を防ぐ方法と、コードを安全に保つための追加のセキュリティ対策を実践的に解説します。

クエリのパラメータ化を実装する

クエリをパラメータ化するには、SQLクエリ内でプレースホルダーを使い、ユーザー入力を表します。プレースホルダーは一時的なマーカーで、SQLエンジンが実際の変数値を割り当てます。クエリの実行時にSQLエンジンが実際の変数をプレースホルダーにバインドしてから実行するため、変数はリテラルデータとして扱われます。

クエリのパラメータ化は、プリペアドステートメントを使っても使わなくても実行できます。この2つの方法の違いは、プリペアドステートメントは事前にコンパイルされ、異なるパラメーターを使って複数回呼び出せることです。 

さらに重要なのは、プリペアドステートメントを使うと、データベースが文を事前にコンパイルするため、より効果的にクエリをパラメータ化でき、実行時間を短縮できる点です。また、実行計画があらかじめ作成されるため、パラメーターが実行計画に影響することはありません。これはセキュリティ体制の強化にもつながります。

クエリを複数回呼び出す必要がある場合、データベースで再コンパイルする必要はありません。プリペアドステートメントに使用するパラメーターを渡すだけです。また、バインドパラメーターを使うと、クエリ全体ではなくパラメーターだけがサーバーに送信されるため、帯域幅を節約できます。 

以下のセクションでは、Webアプリケーションでクエリのパラメータ化を実装する方法を説明します。

前提条件

このデモに沿って進めるには、以下が必要です。

プリペアドステートメントを使わないクエリのパラメータ化 

このデモでは、コマンドラインからユーザー入力を受け取り、その入力をSQLクエリ文の値の1つとして使用する.NETコンソールアプリケーションを使います。

まず、プリペアドステートメントを使わずにクエリをパラメータ化する方法を見てみましょう。 

以下のコードでは、`Console.Readline()`でユーザー名を取得し、クエリを定義します。ユーザー名をクエリに連結する代わりに、`@`文字で示すプレースホルダーを使います。その後、クエリを実行する前に`AddWithValue()`メソッドでユーザー名をバインドします。

try
{
    string? username = Console.ReadLine();
    string query = "INSERT INTO Users (pk_id, name) VALUES (@id, @username)";
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();
        SqlCommand command = new SqlCommand(query, connection);

        // Bind the SQL parameters.
        command.Parameters.AddWithValue("@username", username);
        command.Parameters.AddWithValue("@id", 20);

        // Execute the query
        command.ExecuteNonQuery();
        connection.Close();
    }

}
catch (Exception ex)
{
    //display error message
    Console.WriteLine("Exception: " + ex.Message);
}

クエリを定義した後にパラメーター値を渡すことで、SQL Serverがこれらの値をリテラルとして解釈するようにできます。

プリペアドステートメントを使ったクエリのパラメータ化

同じシナリオを使って、検索クエリをプリペアドステートメントに変更する方法を見てみましょう。

クエリ文字列は変わらず、`@id`と`@username`のプレースホルダーを引き続き使用します。ただし、上の例とは異なり、パラメーターを設定した後、`SqlCommand`オブジェクトの`Prepare`メソッドを呼び出します。

`Prepare()`を呼び出す前に、文の各パラメーターのデータ型を指定してください。また、エラーを防ぐため、サイズのプロパティを最大値に設定する必要があります。

次に、`ExecuteNonQuery`メソッドを使って、指定したパラメーターで`INSERT`文を実行し、以下の例のようにユーザーデータを`Users`テーブルに挿入します。

try
{
    string? username = Console.ReadLine();

    string query = "INSERT INTO Users (pk_id, name) VALUES (@id, @username)";

    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();
        SqlCommand command = new SqlCommand(query, connection);

        // Bind the SQL parameters.
        SqlParameter idParam = new SqlParameter("@id", SqlDbType.Int, 0);
        SqlParameter nameParam = new SqlParameter("@username", SqlDbType.Text, 100);
        idParam.Value = 8;
        nameParam.Value = "First Region";
        command.Parameters.Add(idParam);
        command.Parameters.Add(nameParam);

        // Call Prepare 
        command.Prepare();
        command.ExecuteNonQuery();

        // Change parameter values and call ExecuteNonQuery again.
        command.Parameters[0].Value = 21;
        command.Parameters[1].Value = "Alex";
        command.ExecuteNonQuery();
    }

}
catch (Exception ex)
{
    //display error message
    Console.WriteLine("Exception: " + ex.Message);
}

プリペアドステートメントでは、`Prepare()`メソッドを呼び出すのは1回だけです。その後はパラメーター値を変更して、再コンパイルせずにクエリを複数回実行できます。 

ストアドプロシージャの使用 

プリペアドステートメントを使うと、アプリケーション内でクエリ文を再利用できます。しかし、ストアドプロシージャのように、複数のアプリケーションからプリペアドステートメントを呼び出したり、アクセス制御を実装したりすることはできません。

ストアドプロシージャはデータベースサーバーに保存されたプリコンパイル済みクエリで、複数のアプリケーションから繰り返し呼び出せます。正しく実装すれば、プリペアドステートメントと同等のSQLインジェクション対策になります。ストアドプロシージャの実行権限を管理し、基盤となるテーブルへの直接アクセスを制限することもできます。これにより、SQLインジェクションが発生した場合の影響を軽減できます。

プリペアドステートメントと同様に、ストアドプロシージャもパラメーター化クエリに対応しており、ユーザー入力をSQLコードではなくデータとして扱います。また、データベースはプロシージャに渡されたパラメーターを自動的にサニタイズします。この処理により、攻撃者が悪意のあるコードをデータベースに送信するのを防ぎます。

動的な値をクエリに直接埋め込むのではなく、クエリパラメーターと併せてストアドプロシージャを使う必要があります。ユーザー入力とSQL文を明確に分離することで、SQLエンジンが入力をコードとして解釈するのを防げます。 

例として、データベースに`Id`と`Name`の値を受け取るストアドプロシージャを作成しましょう。

USE db;  
GO  
CREATE PROCEDURE InsertUser
    @Id INT,
    @Name VARCHAR(50)  
AS   
    INSERT INTO Users (pk_id, Name)
    VALUES (@Id, @Name);
GO

SQL Server Management Studioを使用する場合は、クエリエディターでプロシージャを実行し、dbをデータベース名に置き換えてください。

以下は、ストアドプロシージャを使って新しいユーザーをテーブルに追加する例です。

まず、`Console.Readline()`を使って、コマンドラインからユーザー名を入力するよう促します。次に、`SQLConnection`コマンドでデータベースに接続します。その後、上の例で作成した`InsertUser`ストアドプロシージャを呼び出します。 

ストアドプロシージャにパラメーターをバインドするには、`Parameters.AddWithValue`メソッドを使います。この例では、`@id`パラメーターにハードコードした値を指定し、ユーザー入力から取得したユーザー名を`@name`パラメーターに渡します。

try
{
    string? username = Console.ReadLine();
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();

        // Call the stored procedure
        SqlCommand command = new SqlCommand("InsertUser", connection);
        command.CommandType = CommandType.StoredProcedure;

        // Bind the SQL parameters.
        command.Parameters.AddWithValue("@id", SqlDbType.Int).Value = 134;
        command.Parameters.AddWithValue("@name", SqlDbType.VarChar).Value = username;

        // Execute the query
        command.ExecuteNonQuery();
    }
}
catch (Exception ex)
{
    // Display error message
    Console.WriteLine("Exception: " + ex.Message);
}

ストアドプロシージャを呼び出して変数を渡した後、`ExecuteNonQuery`メソッドでクエリを実行します。

データベースクエリに関する追加のセキュリティ対策 

プリペアドステートメントやストアドプロシージャでパラメータ化する以外にも、SQLクエリを安全に保つためのセキュリティ対策があります。

入力のサニタイズと検証を実施する

前述のとおり、攻撃者は入力データを悪用して、データベース上で悪意のあるコードを実行できます。そのため、入力のサニタイズと検証は非常に重要です。 

サニタイズでは、ユーザー提供データから不要な文字(例:`{}`、`'`、`/`)や安全でないHTMLを取り除きます。一方、検証ではユーザー提供データがデータベースで想定される形式かどうかを確認します。たとえば、入力の長さを確認し、長すぎる入力を拒否できます。また、メールアドレスや日付の形式を適用することもできます。この手法により、SQL文を含むよう細工された入力値が攻撃者から送信されるのを防ぎます。

入力のサニタイズと検証は、SQLクエリで使用する入力を制御するのに役立ちます。しかし、万全ではありません。攻撃者は二重エンコードなどの手法で、対策を回避できます。たとえば、入力を一度しかデコードしないフィルターを回避するために、シングルクォート(`'`)を一般的な`%27`ではなく`%2527`にエンコードする可能性があります。そのため、SQLインジェクション対策として入力のサニタイズに頼るのは避け、クエリをパラメータ化したうえで、追加のセキュリティ対策としてサニタイズを行いましょう。

出力をエンコードする

Webアプリケーションにユーザー入力を表示する際は、必ず事前にエンコードしてください。出力エンコードにより、ユーザーデータがHTML、JavaScript、SQLとして解釈されるのを防げます。

許可リストを使う

クエリを常にパラメータ化できるとは限りません。テーブル名やカラム名など、SQLクエリの一部には対応していません。そのような場合は、許可リストを使って入力を検証するのが効果的です。許可リストを使うと、入力で認める文字やパターンを厳密に定義できるため、悪意のある入力を拒否できます。

SQLインジェクション攻撃を防ぐには、拒否リストより許可リストの方が効果的です。許可リストでは許可する入力の種類を明示的に定義する一方、拒否リストには抜け道があり、攻撃者は入力を変化させて簡単に回避方法を見つけられます。許可リストを使えば、受け入れる入力をより適切に管理できます。

最小権限の原則を実践する

SQLインジェクション攻撃が成功した場合の影響を最小限に抑えるため、常に最小権限の原則(PoLP)を適用してください。PoLPでは、ユーザーのアクセス権を業務に必要な範囲に制限します。

たとえば、ユーザーアカウントにテーブルの読み取り権限だけが必要な場合、PoLPに従って、そのテーブルに対する読み取り専用権限のみを付与します。テーブル内の一部のデータだけにアクセスが必要なら、そのデータだけを含むビューを作成し、テーブル全体ではなくビューへのアクセス権をアカウントに付与できます。 

ストアドプロシージャを使用する場合は、アプリケーションに必要なストアドプロシージャだけに実行権限を制限してください。こうすれば、SQLインジェクションが発生しても、攻撃者のアクセスを制限し、被害の可能性を最小限に抑えられます。

Snykでコードを保護する

SQLインジェクションのテストを実施すると、SQLインジェクションの脆弱性に対する保護を強化できます。静的アプリケーションセキュリティテスト(SAST)を自動化するSnyk Codeなどのツールを使えば、セキュリティ上の問題を早期に発見できます。

Snykには、IDE内でコードを分析する脆弱性スキャナーがあります。また、拡張機能マネージャーからVisual Studio Code拡張機能を入手できます。手順に沿ってインストールし、設定してください。

設定が完了したら、サイドバーのSnykロゴをクリックして使用を開始できます。以下の画像のようなメニューバーが表示されます。各メニュー項目にはドロップダウン矢印があり、クリックすると利用を開始できます。ここではCODE SECURITYを使用します。

Open Source Security、Code Security、Configuration、Code Quality、Help & Feedbackを一覧表示した、Snykのダークテーマのナビゲーションメニュー

パラメーター化クエリを使用していない.NETアプリケーションでコードセキュリティ分析を実行すると、プロジェクトにSQLインジェクションの脆弱性があることをSnykが通知します。

次のメソッドは、文字列の書式設定を使ってユーザー入力をクエリに連結しています。

public void withoutParameters(string dataSource, string initialCat, string persistSecurity, string userId, string password)
    {
        string connectionString = $"Data Source={dataSource};Initial Catalog={initialCat};Persist Security Info={persistSecurity};User ID={userId};Password={password}";
        try
        {

            string username = Console.ReadLine();

            using (SqlConnection connection = new SqlConnection(connectionString))
            {
            connection.Open();
            string query = $"INSERT INTO Users(pk_id, name) VALUES (48,'{username}');";
                using (SqlCommand command = new SqlCommand(query, connection))
                {
                    command.Parameters.AddWithValue("@username", username);

                    using (SqlDataReader reader = command.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            Console.WriteLine("{0} {1}", reader.GetString(0), reader.GetString(1));
                        }
                    }
                }
            }
        }
        catch (Exception ex)
            {
                //display error message
                Console.WriteLine("Exception: " + ex.Message);
            }
 }

上記のコードでコードセキュリティ分析を実行すると、以下のスクリーンショットのように、Snykが脆弱性を1件検出します。

SQLコマンドにサニタイズされていないコマンドライン入力が渡され、28行目にSQLインジェクションの脆弱性があることを示す、Snyk Codeの脆弱性パネル

分析レポートは、コマンドライン引数から取得した未サニタイズの入力がSQLクエリで使用されており、SQLインジェクションの脆弱性につながる可能性があることを示しています。Snykは、脆弱性を含む行とファイルを特定します。また、修正例も提示するため、脆弱性をより迅速に修正できます。

まとめ

クエリのパラメータ化は、SQLインジェクション攻撃を防ぐうえで不可欠です。ユーザー入力を実行可能なコードではなくデータとして扱うことで、悪意のあるSQLインジェクションのリスクを効果的に軽減できます。 

これまで、プリペアドステートメントを使用する場合と使用しない場合の、クエリパラメータ化の利点について説明してきました。最初の方法は、SQLロジックとユーザー入力を分離することで、手動でのエスケープやサニタイズを不要にし、より高いセキュリティを実現します。また、クエリが事前にコンパイルされるため、プリペアドステートメントはパフォーマンスの向上にも役立ちます。

プリペアドステートメントに代わる方法として、ストアドプロシージャがあります。クエリパラメータと組み合わせて使用すれば、アクセス権を管理できるため、セキュリティをさらに強化できます。

アプリケーションのセキュリティをさらに高めるには、開発ワークフローにSnykの脆弱性スキャナーを組み込むことができます。コードベースを定期的にスキャンすることで、SQLインジェクションにつながる可能性のある脆弱性を特定できます。このプラクティスにより、開発中に潜在的なセキュリティリスクに対処し、アプリケーションが攻撃者にさらされるリスクを軽減できます。

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。

続きを読む

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSecのご紹介:AI開発のスピードに対応するセキュリティ

AI駆動型開発のスピードに、従来のセキュリティワークフローは追いついていません。SnykのAgentic AppSecは、セキュリティエージェントが脆弱性を継続的に検出、修正、検証し、人が基準を定められるようにします。

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

illustration hero ai
Blog

AIハリケーンが到来

AIはソフトウェア開発とサイバー攻撃の双方を加速させています。リーダーは、エージェントとコードを開発の初期段階から保護し、実行時に制御を徹底するとともに、防御策を独立して検証しなければなりません。