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 度、悪意のあるリクエストを試してみると、動作しないのがわかるでしょう。
参考資料¶
- OWASP: API8 Injection.
- OWASP: SQL Injection Prevention Cheat Sheet.
- SEI CERT Oracle Coding Standard for Java: IDS00-J. Prevent SQL injection.
- Common Weakness Enumeration: CWE-89.