3秒でわかる
SQLの文の形と、そこへ埋める値を分けて送る仕組み。値が文として解釈されないため、SQLインジェクションを根本から防げます。
もう少し詳しく
どういうものか
プリペアドステートメントは、SQLの文の形と、そこに埋める値を別々にデータベースへ渡す仕組みです。まず ? や $1 のような穴あきのSQLを送って解析させ、そのあとで値を送ります。値は最初から「文の一部」ではなく「データ」として扱われるため、中に ' や -- が混ざっていても文の意味が変わりません。
なぜ必要か
文字列連結でSQLを組み立てると、入力された文字がSQLの一部として解釈されます。ログインフォームに ' OR '1'='1 と入れられただけで、条件が常に真になり全ユーザーの行が返ります。これがSQLインジェクションで、情報漏えい事故の代表例です。入力をエスケープして防ぐ方法もありますが、1か所でも書き忘れれば穴になります。値を別便で送るなら書き忘れようがありません。入力を疑う場所が1か所に集まるので、レビューでも見落としが起きにくくなります。
具体例
import sqlite3
conn = sqlite3.connect("shop.db")
cur = conn.cursor()
name = "' OR '1'='1"
# 危険 文字列連結
cur.execute("SELECT * FROM users WHERE name = '" + name + "'")
# 安全 値は別に渡す
cur.execute("SELECT * FROM users WHERE name = ?", (name,))
print(cur.fetchall()) # 0件同じ文を繰り返し実行するときは、解析結果が再利用されるため速度面でも有利になります。
つまずきやすいところ
プレースホルダをテーブル名や列名に使おうとして失敗する例が多く見られます。SELECT * FROM ? は動きません。穴を空けられるのは値の位置だけで、文の構造は先に確定している必要があります。並び替えの列名を画面から受け取るなら、許可リストと突き合わせて選ぶ形にします。もうひとつは IN 句で、要素数の分だけ ? を並べる必要があります。
似た用語との違い
| 語 | 役割 |
|---|---|
| プリペアドステートメント | 文と値を分けて送る仕組み |
| エスケープ | 危険な文字を無害な表記へ書き換える処理 |
| バインド変数 | プレースホルダに渡す値そのものの呼び名 |
覚え方
先に文の設計図を渡し、あとから材料を渡す方式です。材料が文の一部として読み直されることはない、という点だけ押さえておけば使い方を間違えません。
対応する書き方は言語ごとに名前が違いますが、文と値を分けて渡すという中身はどれも同じです。