SQL インジェクション

OWASP カテゴリ: MASVS-CODE: コード品質

概要

SQL インジェクションでは、SQL ステートメントにコードを挿入し、インターフェースが意図的に公開されているわけではない背後にあるデータベースにアクセスして、脆弱性のあるアプリケーションを利用します。この攻撃により、個人データの流出、データベースの内容の破損、さらにはバックエンド インフラストラクチャの侵害が発生する可能性があります。

SQL は、実行前にユーザー入力を連結して動的に作成したクエリを利用するインジェクションに対して脆弱である可能性があります。ウェブやモバイルなど、あらゆる SQL データベース アプリケーションを対象とした SQL インジェクションが、常にウェブ脆弱性の OWASP トップ 10 にランクインしています。この手法は複数の有名な流出事案で使用されています。

この基本的な例では、ユーザーによる注文番号ボックスへのエスケープされていない入力が SQL 文字列に挿入され、次のクエリとして解釈される可能性があります。

SELECT * FROM users WHERE email = 'example@example.com' AND order_number = '251542'' LIMIT 1

このようなコードにより、ウェブ コンソールにデータベース構文のエラーが表示され、アプリが SQL インジェクションに対して脆弱な可能性があることがわかります。注文番号を 'OR 1=1– に置き換えると、データベースはこのステートメントを True と評価し、常に 1 は 1 に等しいため、認証が達成されます。

同様に、次のクエリはテーブルのすべての行を返します。

SELECT * FROM purchases WHERE email='admin@app.com' OR 1=1;

コンテンツ プロバイダ

コンテンツ プロバイダは、1 つのアプリに制限できるほか、他のアプリとの共有のためにエクスポートできる構造化ストレージの仕組みを提供します。権限は、最小権限の原則に基づいて設定する必要があります。エクスポートされた ContentProvider には、読み取りと書き込みの権限を 1 つだけ指定できます。

すべての SQL インジェクションが悪用につながるわけではありません。一部のコンテンツ プロバイダは、すでにリーダーに SQLite データベースへの完全アクセス権を付与しているため、任意のクエリを実行できても、ほとんどメリットはありません。セキュリティの問題の代表的なパターンには、次のようなものがあります。

  • 複数のコンテンツ プロバイダが 1 つの SQLite データベース ファイルを共有している。
    • この場合、各テーブルが一意のコンテンツ プロバイダを対象としている可能性があります。1 つのコンテンツ プロバイダで SQL インジェクションが成功すると、他のテーブルにもアクセスできるようになります。
  • 1 つのコンテンツ プロバイダが同じデータベース内のコンテンツに対して複数の権限を持っている。
    • さまざまな権限レベルのアクセス権を付与する 1 つのコンテンツ プロバイダでの SQL インジェクションは、セキュリティやプライバシーの設定がローカルで回避されることにつながる可能性があります。

影響

SQL インジェクションによって、機密性の高いユーザーデータやアプリデータが漏洩する、認証や認可による制約が回避される、データベースの破壊や削除に対して脆弱になるという可能があります。個人データが漏洩したユーザーに対する危険で持続的な影響もあります。アプリやサービスのプロバイダは、知的財産やユーザーの信頼を失う恐れがあります。

リスクの軽減

置換可能なパラメータ

置換可能なパラメータとして ? を選択句と独立した選択引数の配列で使用すると、ユーザー入力が SQL ステートメントの一部として解釈されるのではなく、クエリに直接バインドされます。

Kotlin

// Constructs a selection clause with a replaceable parameter.
val selectionClause = "var = ?"

// Sets up an array of arguments.
val selectionArgs: Array<String> = arrayOf("")

// Adds values to the selection arguments array.
selectionArgs[0] = userInput

Java

// Constructs a selection clause with a replaceable parameter.
String selectionClause =  "var = ?";

// Sets up an array of arguments.
String[] selectionArgs = {""};

// Adds values to the selection arguments array.
selectionArgs[0] = userInput;

ユーザー入力は、SQL として扱われるのではなくクエリに直接バインドされるため、コード インジェクションは防止されます。

次の詳細な例では、置換可能なパラメータを使用して購入の詳細を取得するショッピング アプリのクエリを示しています。

Kotlin

fun validateOrderDetails(email: String, orderNumber: String): Boolean {
    val cursor = db.rawQuery(
        "select * from purchases where EMAIL = ? and ORDER_NUMBER = ?",
        arrayOf(email, orderNumber)
    )

    val bool = cursor?.moveToFirst() ?: false
    cursor?.close()

    return bool
}

Java

public boolean validateOrderDetails(String email, String orderNumber) {
    boolean bool = false;
    Cursor cursor = db.rawQuery(
      "select * from purchases where EMAIL = ? and ORDER_NUMBER = ?",
      new String[]{email, orderNumber});
    if (cursor != null) {
        if (cursor.moveToFirst()) {
            bool = true;
        }
        cursor.close();
    }
    return bool;
}

PreparedStatement オブジェクトを使用する

PreparedStatement インターフェースは SQL ステートメントをオブジェクトにプリコンパイルします。このオブジェクトは、その後に何度も効率的に実行できます。PreparedStatement はパラメータのプレースホルダとして ? を使用します。そのため、次のコンパイル済みのインジェクションの試みは無効化されます。

WHERE id=295094 OR 1=1;

この場合、295094 OR 1=1 というステートメントは ID の値と解釈され、何も結果が返されない可能性があります。一方、未加工のクエリでは OR 1=1 というステートメントが WHERE 句の別の部分として解釈されます。以下はパラメータ化されたクエリの例です。

Kotlin

val pstmt: PreparedStatement = con.prepareStatement(
        "UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?").apply {
    setString(1, "Barista")
    setInt(2, 295094)
}

Java

PreparedStatement pstmt = con.prepareStatement(
                                "UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?");
pstmt.setString(1, "Barista")
pstmt.setInt(2, 295094)

query メソッドを使用する

次の少し長い例では、query() メソッドの selectionselectionArgs を組み合わせて WHERE 句を作っています。各引数は独立に指定されるため、組み合わせる前にエスケープされて、SQL インジェクションは阻止されます。

Kotlin

val db: SQLiteDatabase = dbHelper.getReadableDatabase()
// Defines a projection that specifies which columns from the database
// should be selected.
val projection = arrayOf(
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
)

// Filters results WHERE "title" = 'My Title'.
val selection: String = FeedEntry.COLUMN_NAME_TITLE.toString() + " = ?"
val selectionArgs = arrayOf("My Title")

// Specifies how to sort the results in the returned Cursor object.
val sortOrder: String = FeedEntry.COLUMN_NAME_SUBTITLE.toString() + " DESC"

val cursor = db.query(
    FeedEntry.TABLE_NAME,  // The table to query
    projection,            // The array of columns to return
                           //   (pass null to get all)
    selection,             // The columns for the WHERE clause
    selectionArgs,         // The values for the WHERE clause
    null,                  // Don't group the rows
    null,                  // Don't filter by row groups
    sortOrder              // The sort order
).use {
    // Perform operations on the query result here.
    it.moveToFirst()
}

Java

SQLiteDatabase db = dbHelper.getReadableDatabase();
// Defines a projection that specifies which columns from the database
// should be selected.
String[] projection = {
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
};

// Filters results WHERE "title" = 'My Title'.
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };

// Specifies how to sort the results in the returned Cursor object.
String sortOrder =
    FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";

Cursor cursor = db.query(
    FeedEntry.TABLE_NAME,   // The table to query
    projection,             // The array of columns to return (pass null to get all)
    selection,              // The columns for the WHERE clause
    selectionArgs,          // The values for the WHERE clause
    null,                   // don't group the rows
    null,                   // don't filter by row groups
    sortOrder               // The sort order
    );

適切に設定された SQLiteQueryBuilder を使用する

SQLiteDatabase オブジェクトに送信するクエリの作成を支援するクラスである SQLiteQueryBuilder を使用することで、アプリの保護を強化できます。推奨される設定は次のとおりです。

Room ライブラリを使用する

android.database.sqlite パッケージには、Android でデータベースを使用するために必要な API が用意されています。ただし、この方法では、低レベルのコードを記述する必要があり、未加工の SQL クエリをコンパイル時に検証できません。データグラフが変化した際には、影響を受けた SQL クエリを手動で更新する必要があります。これは、時間がかかり、エラーが発生しやすい作業です。

高レベルの解決策は、SQLite データベースの抽象化レイヤとして Room 永続ライブラリを使用することです。Room には以下の機能があります。

  • アプリの永続データに接続するためのメイン アクセス ポイントとして機能するデータベース クラス
  • データベースのテーブルを表現するデータ エンティティ
  • アプリがデータのクエリ、更新、挿入、削除に使用できるメソッドを提供する、データ アクセス オブジェクト(DAO)

Room の利点は以下のとおりです。

  • SQL クエリのコンパイル時検証
  • エラーが発生しやすいボイラープレート コードの削減
  • 効率的なデータベース移行

おすすめの方法

SQL インジェクションは強力な攻撃であり、特に大規模で複雑なアプリでは、完全に阻止することが困難な場合があります。データ インターフェースの潜在的な欠陥の重大度を抑えるには、セキュリティに関する次のような事項も考慮する必要があります。

  • パスワード暗号化に、次のような堅牢で一方向性のあるソルト化されたハッシュを使用する。
    • 商用アプリ向けの 256 ビット AES
    • 楕円曲線暗号の場合は 224 ビットまたは 256 ビットの公開鍵サイズ
  • 権限を制限する。
  • データ形式を入念に構成し、データが所定の形式に適合しているか検証する。
  • 可能であれば、個人情報や機密性のあるユーザーデータを保存しないようにする(たとえば、データを送信または保存するのではなくハッシュすることでアプリロジックを実装する)。
  • 機密性のあるデータにアクセスする API とサードパーティ アプリを最小限にする。

参考資料