クリーム色の背景、色付き表示、高コントラスト、大きな文字のほうが読みやすいですか?配色と文字を選ぶと、すべてのページに一度で反映されます。

無料レッスン

SQLインジェクションの対策方法

このレッスンのあとで: 入力で変わらないSQL文を、パラメータと許可リストを使って書ける。

60分のレッスンコマンド3件SQL インジェクションのラボ:弱い検索を攻撃し、クエリを直して確かめる

SQLインジェクション対策とは

SQLインジェクション対策とは、利用者の入力がSQL文の一部として読まれないように、プログラムを書き直すことです。方法は短く言えます。値は文に混ぜず、パラメータとして別に渡します。

大学の履修検索を考えます。講師名の欄に O'Brien と打っただけで検索が壊れるなら、文の作りに問題があります。

このレッスンのゴール

メモ

入力で変わらないSQL文を、パラメータと許可リストを使って書ける。

今日のキーワード

ことばいみれい
パラメータ化クエリ値を文に混ぜず、別に渡す書き方です。
プレースホルダ値が入る場所に書く印です。?
ワイルドカードLIKE で、何にでも合う特別な文字です。%
エスケープ特別な文字を、ふつうの文字として扱わせることです。
ORMSQL文を自分で書かずに、表を扱える道具です。

学生が入力するセルは、このプレビューでは空白です。

たった1つのルール

注意

試してよいのは、自分専用のラボのコピーだけです。このラボは、わざと弱く作った店で、インターネットにはつながっていません。許可のない他人のシステムに同じことをするのは、不正アクセス禁止法に触れる犯罪です。

値はプレースホルダで渡す

パラメータ化クエリ(parameterized query)では、SQL文の値が入る所に ? だけを書きます。この印を プレースホルダ(placeholder)と呼びます。値は、文とは別に渡します。

データベースは、文の形を先に決めます。あとから届いた値は、文字や数としてしか読まれません。

講師名で履修科目を探す

コード: python
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,)安全値は文の外にある

学生が入力するセルは、このプレビューでは空白です。

名前や向きはパラメータにできない

パラメータにできるのは値です。表の名前、列の名前、並べ替えの向きは、パラメータにできません。並べ替えを利用者に選ばせるページは、語を文に入れるしかありません。

安全な方法は、許す語の一覧を自分で持ち、一覧にある語だけを使うことです。ない語は、決めておいた初期値にします。

一覧にある列だけで並べる

コード: python
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 で逃がせば、ふつうの文字として探せます。

ワイルドカードを逃がす

コード: python
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を書く所でも、値はパラメータで渡します。

このラボとソースについて: SQL インジェクションのラボ:弱い検索を攻撃し、クエリを直して確かめる

授業で使いますか?このモジュールを学生に割り当てると、教員用ガイド、自動採点つきの課題、学生ごとのラボを使えます。