flawopen.com/SQLインジェクション/Python
482のようなチケット番号だけを想定しているフォームを思い浮かべてください。SQLインジェクションとは、誰かがそこに数字の代わりに細工した文字列を入力し、システムが「チケット482だけ」ではなく「すべてのチケットを見せて」と答えてしまう現象です。システムが受け取った値が本当に数字だけかどうかを、一度も確認していなかったために起こります。
SQLインジェクションは、ユーザー制御入力が独立した値として渡されるのではなく、データベースクエリのテキストに直接組み込まれることで発生します。文字列をつなぎ合わせてクエリを組み立てるコードでは、攻撃者はクエリの実際の構造を変えてしまうような入力を送り込めます——「1行だけ取得する」クエリを、全行を返すクエリや、テーブルを削除するクエリに変えてしまうのです。
Pythonでは、ほぼ決まって同じ形で現れます。データベース呼び出しを、データベースドライバ組み込みのパラメータプレースホルダーではなく、f文字列や % フォーマット、あるいは単純な + 連結で組み立ててしまうケースです。
2015年、英国の通信事業者TalkTalkは、買収によって引き継いだ古いWebページのSQLインジェクション脆弱性を攻撃者に悪用され、15万人以上の顧客に影響が及ぶ情報漏えいを起こしました。英国のデータ保護当局はTalkTalkに40万ポンドの制裁金を科し、この不備を「防止可能で基本的な失敗」と評しています。
出典:英国情報コミッショナー事務局(ICO)の制裁通知、2016年 —— 詳細は下記「参考資料」を参照。# user_id はリクエストから直接渡される
def get_user(cursor, user_id):
query = f"SELECT * FROM users WHERE id = {user_id}"
cursor.execute(query)
return cursor.fetchone()
# 値は別途渡され、テキストに埋め込まれない
def get_user(cursor, user_id):
query = "SELECT * FROM users WHERE id = %s"
cursor.execute(query, (user_id,))
return cursor.fetchone()
修正版では、クエリのテキストと値を、execute() に対して2つの別々の引数として渡しています。データベースドライバもこれらを分けてデータベースに送信するため、値が付加される前にクエリの構造が確定しており、値に何の文字が含まれていようとSQL構文の一部として解釈されることは決してありません。f文字列ではこれを実現できません。execute() がクエリを受け取る時点で、値はすでにコマンドの一部であったかのようにテキストへ焼き込まれてしまっているからです。
cursor.execute("... WHERE id = %s" % user_id) はf文字列と同じくらい脆弱です。プレースホルダーが安全になるのは、値が execute() 自体の第2引数として渡された場合のみです —— cursor.execute("...WHERE id = %s", (user_id,)) のように書くことで、置換を行うのがPythonの文字列フォーマットではなくドライバになります。
DjangoのORMやSQLAlchemyのクエリビルダーは、通常のクエリでは自動的にパラメータ化を行います。しかし Model.objects.raw() やSQLAlchemyの text() を使い、その生のSQLをf文字列で組み立てた瞬間に、リスクが戻ってきます。
psycopg2(PostgreSQL)はカラムの型に関わらず %s を使い、sqlite3 は ? を使います。あるドライバのドキュメントのプレースホルダー形式を別のドライバにそのまま持ち込むと、静かに失敗します——思い込みで進めず、実際に使っているドライバのパラメータ形式を確認してください。
ORMの通常のクエリAPIを使う限りは正しいですが、raw() や text() を使って自分で文字列を組み立てた瞬間、それは成り立たなくなります。
リスクは実行時の値の型にあるのではなく、クエリがそもそも文字列補間によって組み立てられていること自体にあります。コードのライフサイクルのどこかでその前提が崩れた瞬間、脆弱性はすでにそこで待ち構えています。
手動エスケープはドライバ依存であり、気づきにくい形で間違えやすいものです。パラメータ化クエリは「より厳密なエスケープ」ではなく、値がクエリのテキストの一部に決してならないことで、問題そのものを完全に回避します。
grep -rn "execute(f\"" --include="*.py" .
grep -rn "execute(.*%\s*(" --include="*.py" .
grep -rn "\.raw(\|text(" --include="*.py" .
通常のクエリメソッドについてはそうです。しかし生クエリ用のエスケープハッチは違います——それらは手書きのSQLとまったく同じ安全性しかなく、それ以上ではありません。
いいえ。外部の第三者が実質的に制御できるものはすべて対象です。HTTPヘッダー、アップロードされたファイル名、さらにはアプリケーションが信頼しているサードパーティAPIからの値も含まれます。
できなくはありませんが、脆く、ドライバに依存します。パラメータ化クエリこそが本当の修正であり、より厳密なエスケープではありません。