SQLインジェクション対策とは
SQLインジェクション対策とは、利用者の入力がSQL文の一部として読まれないように、プログラムを書き直すことです。方法は短く言えます。値は文に混ぜず、パラメータとして別に渡します。
大学の履修検索を考えます。講師名の欄に O'Brien と打っただけで検索が壊れるなら、文の作りに問題があります。
このレッスンのゴール
メモ
入力で変わらないSQL文を、パラメータと許可リストを使って書ける。
今日のキーワード
| ことば | いみ | れい |
|---|---|---|
| パラメータ化クエリ | 値を文に混ぜず、別に渡す書き方です。 | |
| プレースホルダ | 値が入る場所に書く印です。 | ? |
| ワイルドカード | LIKE で、何にでも合う特別な文字です。 | % |
| エスケープ | 特別な文字を、ふつうの文字として扱わせることです。 | |
| ORM | SQL文を自分で書かずに、表を扱える道具です。 |
学生が入力するセルは、このプレビューでは空白です。
たった1つのルール
注意
試してよいのは、自分専用のラボのコピーだけです。このラボは、わざと弱く作った店で、インターネットにはつながっていません。許可のない他人のシステムに同じことをするのは、不正アクセス禁止法に触れる犯罪です。
値はプレースホルダで渡す
パラメータ化クエリ(parameterized query)では、SQL文の値が入る所に ? だけを書きます。この印を プレースホルダ(placeholder)と呼びます。値は、文とは別に渡します。
データベースは、文の形を先に決めます。あとから届いた値は、文字や数としてしか読まれません。
講師名で履修科目を探す
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE courses (title TEXT, teacher TEXT)")
db.execute("INSERT INTO courses VALUES (?, ?)", ("情報セキュリティ入門", "O'Brien"))
db.execute("INSERT INTO courses VALUES (?, ?)", ("データベース", "佐藤"))
# teacher を変えて実行します。引用符が入っても、文は壊れません。
teacher = "O'Brien"
rows = db.execute("SELECT title FROM courses WHERE teacher = ?", (teacher,)).fetchall()
print(rows)
4つの書き方を比べる
| 書き方 | 安全か | 理由 |
|---|---|---|
| "... = '" + teacher + "'" | 安全でない | 値が文の一部になる |
| f"... = '{teacher}'" | 安全でない | 見た目は違うが、同じ連結 |
| "... = '%s'" % teacher | 安全でない | 文字の置き換えにすぎない |
| "... = ?", (teacher,) | 安全 | 値は文の外にある |
学生が入力するセルは、このプレビューでは空白です。
名前や向きはパラメータにできない
パラメータにできるのは値です。表の名前、列の名前、並べ替えの向きは、パラメータにできません。並べ替えを利用者に選ばせるページは、語を文に入れるしかありません。
安全な方法は、許す語の一覧を自分で持ち、一覧にある語だけを使うことです。ない語は、決めておいた初期値にします。
一覧にある列だけで並べる
SORTS = ["title", "teacher"]
def order_clause(choice):
if choice in SORTS:
return " ORDER BY " + choice
return " ORDER BY title"
# choice を "teacher" にして、次に "teacher; DROP" にして、実行します。
print(order_clause("title"))
print(order_clause("teacher; DROP"))
%を含む値は、全件に合ってしまう
LIKE の中で、% は「何文字でも」、_ は「1文字」を表します。値をパラメータにしても、値の中の % はワイルドカードとして働きます。
SQLの書き換えではありませんが、全件が返るなど、想定外の結果になります。ESCAPE で逃がせば、ふつうの文字として探せます。
ワイルドカードを逃がす
def like_text(text):
# 先に逃がす記号そのものを、次に % と _ を逃がします。
text = text.replace("\\", "\\\\")
text = text.replace("%", "\\%")
text = text.replace("_", "\\_")
return "%" + text + "%"
# text を "50%" や "a_b" に変えて、実行します。
print(like_text("50%"))
守りを何枚も重ねる
| 層 | することと、効くこと |
|---|---|
| 検証 | 想定外の値を、入口で断る |
| パラメータ化クエリ | 値が文の一部にならない |
| 権限の小さい口座 | 破られても、消せない、他の表を読めない |
| 短いエラー | 内部の作りを、攻撃する人に見せない |
学生が入力するセルは、このプレビューでは空白です。
ラボの中で、安全な書き方を見る
ラボを起動して、次の3つを実行しましょう。どれも見るだけです。1つ目は、ラボのコンテナの Python に、敵意のある文字を「値」として入れます。メモリ上の表だけが相手です。
2つ目は % を含む値を、逃がす前と後で検索します。3つ目は、店に % を送ります。
敵意のある文字を値にする
docker exec app python -c 'import sqlite3; db = sqlite3.connect(":memory:"); db.execute("CREATE TABLE t (n TEXT)"); db.execute("INSERT INTO t VALUES (?)", ("x" + chr(39) + " OR 1=1 --",)); print(db.execute("SELECT n FROM t WHERE n = ?", ("x" + chr(39) + " OR 1=1 --",)).fetchall())'ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
%を逃がす前と後
docker exec app python -c 'import sqlite3; db = sqlite3.connect(":memory:"); db.execute("CREATE TABLE t (n TEXT)"); db.executemany("INSERT INTO t VALUES (?)", [("50%off",), ("500yen",)]); print(db.execute("SELECT n FROM t WHERE n LIKE ?", ("%50%",)).fetchall()); print(db.execute("SELECT n FROM t WHERE n LIKE ? ESCAPE char(92)", ("%50\\%%",)).fetchall())'ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
店に%を送る
curl -s -G --data-urlencode "q=%" http://app:8000/searchここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
3つの答えを読む
1つ目は [("x' OR 1=1 --",)] を表示します。敵意のある文字は、ただの文字として、そのまま読み出されました。2つ目は、1行目に2つの行、2行目に 50%off だけを表示します。
3つ目は、公開の5つの商品を全部表示します。% は、値でも「何にでも合う」からです。
よくある間違い
注意
「ORM を使っているから、SQLインジェクションは起きない」と考える人がいます。それは違います。ORM の中で、文を連結して作る生のSQLを書けば、同じ穴が開きます。生のSQLを書く所でも、値はパラメータで渡します。