SQLインジェクション

SQLインジェクション

説明

SQLインジェクションは、信頼できないデータを独立した値として渡さず、実行するSQLテキストの一部にしたときに発生します。攻撃者はクエリの構造を変え、条件を回避したり、アプリケーションのデータベースアカウントが扱えるデータを読み取り・変更・削除したりする可能性があります。

execute()などのAPIや生のSQLを使うこと自体が脆弱性ではありません。実行時に、信頼できない値とSQLの構造を分離していないことが問題です。

想定される影響

  • 情報漏えい: 条件の回避やSQL構文の挿入により、アプリケーションのアカウントが読める情報が公開されるおそれがあります。
  • データの変更・削除: 書き込み権限がある場合は、データの完全性が損なわれる可能性があります。
  • 認証・認可の回避: アクセス判断に使うクエリの条件を回避されるおそれがあります。
  • 追加の影響: サービス拒否やOSコマンドの実行につながるかは、データベースの種類、有効な機能、アカウント権限によって異なります。

対処方法

  • APIが定めるプレースホルダーとパラメーター引数を使い、値をSQLテキストから分離してください。ドライバーによって?、%s、%(name)s、:nameなどの形式が異なります。該当APIの仕様に従い、プレースホルダーを引用符で囲まないでください。
  • テーブル名、列名、並び順などのSQL構造は値のパラメーターでは指定できません。クエリビルダーを優先し、psycopg.sql.Identifierなどの識別子構成APIを使うか、少数の許可された選択肢を開発者が管理するSQL断片に対応付けてください。
  • SQLAlchemyではCoreまたはORMの式を優先してください。テキストSQLが必要な場合は、text()のテンプレートを固定し、値をバインドパラメーターで渡してください。exec_driver_sql()には、基盤となるドライバーのプレースホルダー形式を使ってください。
  • DjangoではORMを優先してください。Manager.raw()やカーソルを使う場合は、値をparamsで渡してください。
  • Peeweeのexecute_sql()とpandasのread_sql_query()でも、値はparamsで渡してください。
  • 数値など形式が制限された入力も、可能なら型変換後にバインドしてください。パターン検査、エスケープ用ヘルパー、その関数名、クライアントが制御する許可リストをパラメーター化の代わりにしないでください。
  • 悪用時の影響を抑えるため、データベースアカウントには必要最小限の権限だけを付与してください。

例

変更前

python
import sqlite3

from flask import request

def find_users():
    keyword = request.args.get("q", "")
    sort_column = request.args.get("sort", "id")

    with sqlite3.connect("app.db") as connection:
        cursor = connection.cursor()
        # 値と識別子の両方をSQLテキストへ直接挿入
        sql = (
            "SELECT id, name, created_at FROM users "
            f"WHERE name LIKE '%{keyword}%' ORDER BY {sort_column} DESC"
        )
        return cursor.execute(sql).fetchall()

変更後

python
import sqlite3

from flask import request

def find_users():
    keyword = request.args.get("q", "")
    choice = request.args.get("sort", "id")

    # 入力は選択だけに使い、SQL識別子はコード内の固定リテラルから取得
    if choice == "name":
        sort_column = "name"
    elif choice == "created_at":
        sort_column = "created_at"
    else:
        sort_column = "id"

    # SQLiteの?プレースホルダーは値にのみ使用
    sql = (
        "SELECT id, name, created_at FROM users "
        "WHERE name LIKE ? "
        f"ORDER BY {sort_column} DESC"
    )

    with sqlite3.connect("app.db") as connection:
        cursor = connection.cursor()
        return cursor.execute(sql, (f"%{keyword}%",)).fetchall()

説明:

  • 変更前: 検索値と並べ替え列の両方をSQLテキストに挿入するため、SQL構文として解釈される可能性があります。
  • 変更後: 検索値をexecute()の第2引数で渡します。値としてバインドできない列名は、サーバーコード内の固定リテラルから選びます。不明な選択値には既定のidを使います。
  • 許可リスト自体をリクエストパラメーター、本文、クッキーから作ると、攻撃者が検証方針を制御できてしまいます。

適用時の注意点

独自のデータベースラッパーや複数の関数を通るクエリでも、最終的な実行までSQLテキストと値を分離してください。テーブル名とクエリの両方を受け取るAPIでは用途を区別し、動的なクエリの値は該当ドライバーの方式でバインドしてください。

参考資料