コンテンツにスキップ

SQL Injection (SQL インジェクション)

概要

文字列の連結によってデータベース クエリーが作成され、サブ文字列がユーザー入力から取得される場合、攻撃者が悪意のあるデータベース クエリーを実行できる可能性があります。

推奨事項

文字列の連結によって SQL クエリーを構築するのではなく、SQL プリペアド ステートメントを使用することを推奨します。通常、プリペアド ステートメントには、SQL クエリーのうち変数で置き換えられる部分ごとに疑問符 (?) で表されるワイルドカードが含まれます。後でプリペアド ステートメントが実行されるとき、クエリー内の各ワイルドカードに対して値が指定される必要があります。

サンプル

次のサンプルには、単純な FastAPI python 認証 API があります。API はインメモリ sqlite データベースを使用してユーザー認証情報を格納します (パスワードはプレーンテキストで格納されます 😱 -- 実際はこのようにしないでください)。API は 2 つのクエリー パラメーター email および password を受け取る単一のエンドポイント /login を公開します。エンドポイントはこれらのパラメーターを安全ではない方法で処理し、安全ではない文字列フォーマットを使って SQL クエリーを構築します。

from fastapi import FastAPI
import sqlite3

app = FastAPI()
DB = sqlite3.connect(':memory:')

@app.on_event("startup")
async def init_db():
    cur = DB.cursor()
    cur.execute("CREATE TABLE users (email text, password text)")
    cur.execute("INSERT INTO users VALUES ('jholden@roci.space', 'tachi')")
    DB.commit()

@app.get("/login")
async def login(email: str, password: str):
    cur = DB.cursor()
    cur.execute("SELECT * FROM users WHERE email = '%s' AND password = '%s'" % (email, password))
    user = cur.fetchone()
    cur.close()
    if user:
        # ... Set cookie
        return '👋 Welcome back %s!' % (user[0],)
    return '🚨Bad credentials!'

通常の入力では、エンドポイントは期待どおり動作します。

$ curl "localhost:8081/login?email=test&password=please-ignore"
"🚨 Bad credentials!"
$ curl "localhost:8081/login?email=jholden@roci.space&password=tachi"
"👋 Welcome back jholden@roci.space!"

しかし、特別に細工された入力によって、攻撃者はパスワードを知らなくても認証が可能になります。

$ curl "localhost:8081/login?email=attacker&password='OR'1'='1"
"👋 Welcome back jholden@roci.space!"

エンドポイントは、パスワードなしで攻撃者を別のユーザーとしてログインさせたのでしょうか? 何が起きたのでしょうか? SQL クエリーは文字列フォーマットを使用して構築されています。SQL クエリーに攻撃者からの入力を挿入すると、次のようになります。

SELECT * FROM users WHERE email = 'attacker' AND password = '1'OR'1'='1'

OR '1'='1' があるため、WHERE 句は常に true となり、クエリーはテーブルのすべての行を返します。このケースでは、データベースには 1 人のユーザーだけしかいないので、そのユーザーが攻撃者によってハックされます。SQL インジェクションを使用すると、通常、攻撃者はデータベース コンテンツ全体を入手でき、大きな損害の可能性があるデータ リークにつながります。

では、どうすればこのサンプルを修正できるでしょうか? プリペアド ステートメントを使用します。

cur.execute('SELECT * FROM users WHERE email = ? AND password = ?', (email, password))

違いは微妙です。'%s'? に置き換えただけのように見えます。依然としてフォーマット文字列のように見えますが、なぜこちらは安全であちらは安全ではないのでしょうか? 答えは、? 文字の変換を行うコード (データベース自体またはライブラリ) が適切に変数をサニタイズし、SQL インジェクションが起こらないようにするからです。もう 1 度、悪意のあるリクエストを試してみると、動作しないのがわかるでしょう。

参考資料