SQL インジェクション

適用対象: SQL Server Azure SQL データベース Azure SQL Managed Instance Azure Synapse Analytics Analytics Platform System (PDW)

SQL インジェクションとは、悪意のあるコードを挿入し、後で SQL Server のデータベース エンジンのインスタンスに渡すことで解析と実行を行う攻撃です。 データベース エンジンは、構文的に有効であれば受信したクエリをすべて実行するため、SQL ステートメントを構築するすべてのプロシージャにおいて、インジェクションの脆弱性を検証する必要があります。 高いスキルを持った攻撃者は、その気になればパラメーター化されたデータでさえも操作できます。

SQL インジェクションのしくみ

SQL インジェクションは主に、SQL コマンドと連結されて実行されるユーザー入力変数にコードを直接挿入することにより行われます。 それほど直接的ではない攻撃では、悪意のあるコードが、テーブル内の記憶領域に格納される文字列に挿入されたり、メタデータとして挿入されたりします。 格納された文字列が後で動的な SQL コマンドに連結された場合、悪意のあるコードが実行されます。

インジェクション プロセスは、途中でテキスト文字列を終了し、新しいコマンドを追加することによって行われます。 挿入されたコマンドが実行される前に余分な文字列が追加されている可能性があるため、犯罪者は挿入される文字列をコメントマーク -- で終了させます。 後続のテキストは実行時には無視されます。

次のスクリプトは、単純な SQL インジェクションを示しています。 このスクリプトは、ハードコーディングされた文字列とユーザーが入力した文字列を連結して、SQL クエリを作成します。

var ShipCity;
ShipCity = Request.form ("ShipCity");
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";

ユーザーは、都市の名前を入力するように要求されます。 ユーザーが「Redmond」と入力した場合、スクリプトによって構築されたクエリの例は次のようになります:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';

ただし、ユーザーが次のテキストを入力するとします:

Redmond';drop table OrdersTable--

この場合、スクリプトは次のようなクエリを構築します:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';drop table OrdersTable--'

セミコロン (;) は、前のクエリの終了と次のクエリの開始の区切りを示します。 ダブルハイフン (--) は、現在行の残りの部分はコメントであるため無視されることを意味します。 変更されたコードが構文的に正しい場合、このコードはサーバーによって実行されます。 データベース エンジンがこのステートメントを処理する場合、ShipCityRedmond である OrdersTable にあるレコードをすべて選択します。 次に、データベース エンジンが OrdersTable をドロップします。

挿入された SQL コードが構文的に正しい限り、改ざんをプログラムによって検出するのは不可能です。 そのため、すべてのユーザー入力を検証し、使用サーバーで作成された SQL コマンドを実行するコードを注意深くレビューする必要があります。 この記事の次のセクションでは、コーディングのベスト プラクティスについて説明します。

すべての入力を検証する

型、長さ、形式、および範囲をテストすることによってユーザー入力を必ず検証してください。 悪意のある入力に対する予防措置を実装するには、アプリケーションのアーキテクチャおよび配置シナリオについて検討する必要があります。 セキュリティで保護された環境で実行するようにデザインされたプログラムでも、セキュリティで保護されていない環境にコピーされる可能性があることに注意してください。 次の提案をベスト プラクティスとして検討してください。

  • アプリケーションによって受信されるデータのサイズ、型、内容を推測で処理しない。 たとえば、次のような評価を行う必要があります。

    • 郵便番号の入力がアプリケーションから期待されている場所に対して、ユーザーの誤りあるいは悪意のあるユーザーによって 2-GB のビデオ ファイルが入力された場合、アプリケーションはどのような動作をするか。

    • DROP TABLE ステートメントがテキスト フィールドに埋め込まれている場合、アプリケーションはどのような動作をするか。

  • 入力のサイズとデータ型をテストし、適切な制限を適用します。 これは、意図的なバッファー オーバーランを防ぐのに役立ちます。

  • 文字列変数の内容をテストし、予測される値のみを受け入れる。 バイナリ データ、エスケープ シーケンス、およびコメント文字を含む入力は拒否します。 これは、スクリプト インジェクションを防ぐのに役立ち、バッファー オーバーランをねらった攻撃に対する防御にもなります。

  • XML ドキュメントを扱う場合、入力時にすべてのデータをスキーマに照らして検証します。

  • Transact-SQL ステートメントをユーザー入力から直接構築しないこと。

  • ストアド プロシージャを使用して、ユーザー入力を検証する。

  • 多層環境では、信頼関係ゾーンへ入る前にすべてのデータを検証する必要がある。 検証プロセスをパスしないデータは拒否し、直前の層にエラーを返す必要があります。

  • 複数層の検証を実装する。 軽い悪意を持つユーザーに対する予防策は、決然たる攻撃者に対しては有効ではありません。 より適切な実践方法は、ユーザー インターフェイスを介した入力時に検証を行い、その後の信頼境界を越えるすべてのポイントでも検証を行うことです。

    たとえば、クライアント側アプリケーションでのデータ検証によって、単純なスクリプト インジェクションを防ぐことができます。 ただし、次の層で、この入力が既に検証済みであると推測されると、クライアントを迂回する能力を持つ悪意のあるユーザーは、システムへ無制限にアクセスできることになります。

  • 検証されていないユーザー入力は決して連結しないでください。 文字列の連結は、スクリプト インジェクションの最初の段階です。

  • ファイル名の作成に使用できるフィールドでは、次の文字列を受け付けないでください: AUXCLOCK$COM1 から COM8CONCONFIG$LPT1 から LPT8NUL および PRN

可能であれば、次の文字を含む入力は受け入れないでください。

入力文字 Transact-SQL での意味
; クエリの区切り記号。
' 文字データ文字列の区切り記号。
-- 単一行コメントの区切り記号。 -- 以降、その行の末尾までのテキストは、サーバーによって評価されません。
/*** ... ***/ コメントの区切り記号。 /**/ の間にあるテキストについては、サーバーによる評価は行われません。
xp_ xp_cmdshellなど、カタログ拡張ストアド プロシージャ名の先頭に使用します。

タイプ セーフ SQL パラメーターを使用する

データベース エンジンの Parameters コレクションは、型チェックおよび長さの検証に使用できます。 Parameters コレクションを使用する場合、入力は実行可能コードとしてではなくリテラル値として扱われます。 Parameters コレクションを使用することの他の利点は、型チェックおよび長さのチェックを適用できることです。 範囲外の値により、例外がトリガーされます。 次のコード フラグメントは、Parameters コレクションの使用方法を示しています:

SqlDataAdapter myCommand = new SqlDataAdapter("AuthorLogin", conn);
myCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
    SqlDbType.VarChar, 11);
parm.Value = Login.Text;

この例では、 @au_id パラメーターは実行可能コードとしてではなくリテラル値として扱われます。 この値は型および長さについてチェックされます。 @au_id の値が指定された型および長さの制約に従っていない場合は、例外がスローされます。

ストアド プロシージャとパラメーター化された入力を使用する

ストアド プロシージャがフィルターされていない入力を使用する場合、このストアド プロシージャは SQL インジェクションの影響を受けやすくなります。 たとえば、次のコードには脆弱性があります。

SqlDataAdapter myCommand =
    new SqlDataAdapter("LoginStoredProcedure '" + Login.Text + "'", conn);

ストアド プロシージャを使用する場合、パラメーターをストアド プロシージャの入力として使用する必要があります。

動的な SQL を使用した Parameters コレクションを使用する

ストアド プロシージャを使用できない場合でも、次のコード例に示すようにパラメーターを使用することができます。

SqlDataAdapter myCommand = new SqlDataAdapter(
    "SELECT au_lname, au_fname FROM Authors WHERE au_id = @au_id", conn);
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
    SqlDbType.VarChar, 11);
parm.Value = Login.Text;

入力をフィルターする

入力をフィルターすると、エスケープ文字が削除されることで SQL インジェクションの防御に役立ちます。 ただし、問題となり得る文字は数が多いため、フィルタリングは信頼性の高い防御策にはなりません。 次の例では、文字の文字列区切り記号を検索しています。

private string SafeSqlLiteral(string inputSQL)
{
    return inputSQL.Replace("'", "''");
}

LIKE 句

LIKE 句を使用する場合、ワイルドカード文字もエスケープする必要があることに注意してください:

s = s.Replace("[", "[[]");
s = s.Replace("%", "[%]");
s = s.Replace("_", "[_]");

SQL インジェクションのコードを確認する

EXECUTEEXEC、または sp_executesqlを呼び出すすべてのコードを確認する必要があります。 次のようなクエリを使用すると、これらのステートメントを含むプロシージャの識別に役立てることができます。 このクエリは、 EXECUTE または EXECという語の後に 1 ~ 4 個のスペースがあるかどうかをチェックします。

SELECT object_Name(id)
FROM syscomments
WHERE UPPER(TEXT) LIKE '%EXECUTE (%'
    OR UPPER(TEXT) LIKE '%EXECUTE  (%'
    OR UPPER(TEXT) LIKE '%EXECUTE   (%'
    OR UPPER(TEXT) LIKE '%EXECUTE    (%'
    OR UPPER(TEXT) LIKE '%EXEC (%'
    OR UPPER(TEXT) LIKE '%EXEC  (%'
    OR UPPER(TEXT) LIKE '%EXEC   (%'
    OR UPPER(TEXT) LIKE '%EXEC    (%'
    OR UPPER(TEXT) LIKE '%SP_EXECUTESQL%';

QUOTENAME() および REPLACE() でのパラメーターをラップする

選択された各ストアド プロシージャで、動的な Transact-SQL で使用されるすべての変数が正しく処理されることを確認します。 ストアド プロシージャの入力パラメーターから取得するデータ、またはテーブルから読み取るデータは、QUOTENAME() または REPLACE() でラップする必要があります。 QUOTENAME() に渡される @variable の値のデータ型は sysname であり、文字列の最大長は 128 文字であることに注意してください。

@variable 推奨ラッパー
セキュリティ保護可能なリソースの名前 QUOTENAME(@variable)
<= 128 文字の文字列 QUOTENAME(@variable, '''')
> 128 文字の文字列 REPLACE(@variable,'''', '''''')

この方法を使用すると、SET ステートメントを次のように変更できます:

-- Before:
SET @temp = N'SELECT * FROM authors WHERE au_lname ='''
    + @au_lname + N'''';

-- After:
SET @temp = N'SELECT * FROM authors WHERE au_lname = '''
    + REPLACE(@au_lname, '''', '''''') + N'''';

データの切り捨てによって有効になるインジェクション

変数に代入されるすべての動的な Transact-SQL は、その変数に割り当てられているバッファーよりも大きい場合は切り捨てられます。 予期しない長さの文字列をストアド プロシージャに渡すことで、強制的にステートメントの切り捨てを行うことができれば、攻撃者が結果を操作することも可能になります。 たとえば、次の例のストアド プロシージャは、切り捨てによって有効になるインジェクションの影響を受けやすくなります。

この例では、最大長が 200 文字の @command バッファーがあります。 'sa'のパスワードを設定するには、合計 154 文字が必要です。UPDATE ステートメントの場合は 26 文字、WHERE 句の場合は 16 文字、'sa' の場合は 4 文字、QUOTENAME(@loginname) で囲われた引用符 の場合は 2 文字となります: 200 - 26 - 16 - 4 - 2 = 154。 ただし、@newsysname として表されているため、この変数は 128 文字しか保持できません。 私たちは、@new でいくつかの単一引用符を渡すことによってこれを克服することができます。

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variable.
DECLARE @command VARCHAR(200)

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password=' + QUOTENAME(@new, '''')
    + ' WHERE username=' + QUOTENAME(@loginname, '''') 
    + ' AND password=' + QUOTENAME(@old, '''')

-- Execute the command.
EXEC (@command);
GO

攻撃者は 128 文字バッファーに 154 文字を渡す場合、古いパスワードがわからなくても、sa の新しいパスワードを設定できます。

EXEC sp_MySetPassword 'sa',
    'dummy',
    '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012'''''''''''''''''''''''''''''''''''''''''''''''''''

このため、コマンド変数には大きなバッファーを使用するか、動的 Transact-SQL を直接 EXECUTE ステートメント内で実行する必要があります。

QUOTENAME(@variable, '''') および REPLACE() 使用時の切り捨て

QUOTENAME() および REPLACE() から返される文字列は、割り当てられている領域よりも大きくなると、暗黙に切り捨てられます。 次の例で作成されるストアド プロシージャは、行われる可能性がある処理を示しています。

この例では、@login@oldpassword、および @newpassword のバッファー サイズが 128 文字のみでも、QUOTENAME() は最大 258 文字を返すことができるため、一時変数に格納されているデータは切り捨てられます。 @new に 128 文字が含まれている場合は、その結果 @newpassword123... n となる可能性があり、n は127 番目の文字となります。 QUOTENAME() によって返される文字列は切り捨てられるため、次のステートメントのようになります:

UPDATE Users SET password ='1234...[127] WHERE username=' -- other stuff here

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);

SET @login = QUOTENAME(@loginname, '''');
SET @oldpassword = QUOTENAME(@old, '''');
SET @newpassword = QUOTENAME(@new, '''');

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users set password = ' + @newpassword
    + ' WHERE username = ' + @login
    + ' AND password = ' + @oldpassword;

-- Execute the command.
EXEC (@command);
GO

このため、次のステートメントでは、すべてのユーザーのパスワードを前のコードで渡された値に設定します。

EXEC sp_MyProc '--', 'dummy', '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'

REPLACE() を使用すると、割り当てられたバッファー領域よりも大きくすることで強制的に文字列の切り捨てを行うことができます。 次の例で作成されるストアド プロシージャは、行われる可能性がある処理を示しています。

この例では、@login@oldpassword、および @newpassword に割り当てられたバッファーが 128 文字しか保持できないため、データは切り捨てられますが、QUOTENAME() では最大 258 文字を返すことができます。 @new に 128 文字が含まれている場合は、@newpassword'123...n' となる可能性があり、n は127 番目の文字となります。 QUOTENAME() によって返される文字列は切り捨てられるため、次のステートメントのようになります:

UPDATE Users SET password='1234...[127] WHERE username=' -- other stuff here

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);

SET @login = REPLACE(@loginname, '''', '''''');
SET @oldpassword = REPLACE(@old, '''', '''''');
SET @newpassword = REPLACE(@new, '''', '''''');

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password = '''
    + @newpassword + ''' WHERE username = '''
    + @login + ''' AND password = ''' + @oldpassword + '''';

-- Execute the command.
EXEC (@command);
GO

QUOTENAME() と同様に、すべての場合に対して十分な大きさである一時変数を宣言することで、REPLACE() による文字列の切り捨てを回避できます。 可能であれば、動的 Transact-SQL 内で QUOTENAME() または REPLACE() を直接呼び出すことをお勧めします。 あるいは、必要なバッファー サイズを次のように計算できます。 @outbuffer = QUOTENAME(@input)の場合、 @outbuffer のサイズは 2 * (len(@input) + 1)にする必要があります。 前の例のように、 REPLACE() を使用し、二重引用符を繰り返すときは、バッファー サイズは 2 * len(@input) で十分です。

次の計算は、すべての場合に対応します。

WHILE LEN(@find_string) > 0, required buffer size =
    ROUND(LEN(@input) / LEN(@find_string), 0)
        * LEN(@new_string) + (LEN(@input) % LEN(@find_string))

QUOTENAME(@variable, ']') 使用時の切り捨て

データベース エンジンのセキュリティ保護可能なリソースの名前が、形式 QUOTENAME(@variable, ']') を使用するステートメントに渡されると、切り捨てが発生する可能性があります。 このシナリオを以下の例で説明します。

この例では、@objectname は、2 * 258 + 1 文字を許可する必要があります。

CREATE PROCEDURE sp_MyProc
    @schemaname SYSNAME,
    @tablename SYSNAME
AS
-- Declare a variable as sysname. The variable will be 128 characters.
DECLARE @objectname SYSNAME;

SET @objectname = QUOTENAME(@schemaname) + '.' + QUOTENAME(@tablename);
    -- Do some operations.
GO

sysname の値を連結する場合、値ごとに最大 128 文字を十分保持できる大きさの一時変数を使用する必要があります。 可能であれば、動的 Transact-SQL 内で QUOTENAME() を直接呼び出します。 あるいは、前述のように必要なバッファー サイズを計算できます。