アポストロフィで出るエラー
洋書を売る小さなネットショップがあります。お客さんが検索窓に「Alice's Adventures」と入力しました。
画面には、見慣れない英語のエラーが出ました。店主は小さな不具合だと思っています。でも本当は、検索窓に打った文字が、店の質問文の一部になっていたのです。
たった1つのルール
注意
攻撃してよいのは、自分専用のラボのコピーだけです。このラボは、わざと弱く作った店で、インターネットにはつながっていません。許可のない他人のシステムを攻撃すると、不正アクセス禁止法に触れる犯罪になります。
このレッスンのゴール
メモ
打ち込まれた文字で変わらないように、検索の質問文を直せる。
今日のキーワード
| ことば | いみ | れい |
|---|---|---|
| データベース | データをまとめて持ち、質問に答える仕組みです。 | |
| クエリ | プログラムがデータベースに送る質問文です。 | SELECT name FROM products |
| SQLインジェクション | 打ち込んだ文字で、クエリを書き換える攻撃です。 | |
| パラメータ | クエリの外に分けて送る値です。 | ? |
| 文字列 | プログラムの中の文字のまとまりです。 | "pen" |
| 入力 | 利用者が画面に打ち込む、または送る内容です。 |
学生が入力するセルは、このプレビューでは空白です。
つなげたクエリは書き換えられる
クエリを、決まった前半と利用者の入力をつなげて作るプログラムがあります。データベースには、長い1本の文字列が届きます。
どこまでがプログラマーの文で、どこからが利用者の文字か、データベースには見分けられません。入力に引用符が入っていると、文字列がそこで終わってしまい、後ろがクエリの続きとして読まれます。
プログラムが作るクエリ
# term を "Notebook" や "Tom's" に変えて、実行します。
term = "ペン"
start = "SELECT name, price FROM products WHERE name LIKE '%"
query = start + term + "%'"
print(query)
例: 3つの検索
| 入力 | データベースに届くクエリ | 起こること |
|---|---|---|
| ペン | ... LIKE '%ペン%' | 思ったとおりに動く |
| Tom's | ... LIKE '%Tom's%' | 引用符で文字列が終わり、クエリが壊れる |
| x' AND 1=2 -- | ... LIKE '%x' AND 1=2 --%' | 入力した人が、条件を自分で足せてしまう |
学生が入力するセルは、このプレビューでは空白です。
ラボの店を見てみる
ラボには、この弱点を持つ本物の店があります。ラボを起動して、次の3つを実行しましょう。どれも見るだけです。
1つ目はふつうの検索です。2つ目はアポストロフィ入りの名前で、店はこれをうまく扱えません。3つ目は、クエリを作っているコードの1行を表示します。
ふつうの検索
curl -s -G --data-urlencode "q=pen" http://app:8000/searchここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
アポストロフィ入りの名前
curl -s -G --data-urlencode "q=Baker's" http://app:8000/searchここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
クエリを作っている行
docker exec app grep -n "rows = db.execute" /tmp/app/app.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
3つの答えを読む
1つ目は Pen 1.2 と、名前と値段を表示します。2つ目は database error で始まる行を表示します。店は、あなたのアポストロフィをそのままクエリに入れたのです。
3つ目は、行番号と長いコードを1行表示します。その中の + を探してください。+ のたびに、入力がクエリへつなげられています。
パラメータで文字を分ける
直し方は パラメータ です。クエリには値が入る場所に ? だけを書き、打たれた文字は別にして送ります。現場では、プレースホルダやプリペアドステートメント(prepared statement)とも呼びます。
データベースは先にクエリを読み、形を決めてしまいます。あとから届く文字は、値としてしか扱われません。引用符が入っていても、クエリは終わりません。
パラメータを使った検索
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE products (name TEXT, price REAL)")
db.execute("INSERT INTO products VALUES (?, ?)", ("Tom's ノート", 480))
db.execute("INSERT INTO products VALUES (?, ?)", ("ペン", 120))
# term を変えて実行します。何を打っても、クエリは途中で終わりません。
term = "Tom's"
rows = db.execute("SELECT name, price FROM products WHERE name LIKE ?", ("%" + term + "%",)).fetchall()
print(rows)
よくある間違い
注意
「アポストロフィを消せば安全だ」と考える人がいます。それは違います。取りこぼしがあれば、そこが穴になります。パラメータは、入力をクエリに混ぜないから安全です。どんな入力でも同じです。値はすべてパラメータで渡しましょう。
パラメータにできないもの
パラメータは値の代わりになります。でも、列の名前や並べ替えの向きの代わりにはなりません。値段順や名前順を選ばせるページは、その語をクエリに入れるしかありません。
安全な方法は、許す語の短い一覧を作ることです。選ばれた語が一覧になければ、決めておいた初期値を使います。
一覧にある語だけを使う
ALLOWED = ["name", "price"]
# choice を "price" にして、次に "stock; drop" にして、実行します。
choice = "name"
if choice in ALLOWED:
column = choice
else:
column = "name"
print("ORDER BY " + column)