説明
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では用途を区別し、動的なクエリの値は該当ドライバーの方式でバインドしてください。