検索欄から全員の注文が見えた
神戸の家具店が、注文を調べられるページを作りました。お客さまが名前を入れると、自分の注文だけが出ます。
ところが、ある記号を混ぜて入力すると、全員の注文が表示されてしまいました。
このレッスンでは、入力欄から入る代表的な攻撃を二つ見ます。どちらも守り方は決まっています。
このレッスンのゴール
メモ
インジェクションとクロスサイトスクリプティングの仕組みを説明できる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| SQL インジェクション | 入力に SQL を混ぜ、データベースへの命令を変える攻撃です。 | |
| プレースホルダー | SQL の中で、値をあとから入れる場所です。 | ? |
| XSS | 投稿にスクリプトを混ぜ、ほかの人のブラウザで動かす攻撃です。 | |
| エスケープ | 特別な記号を、ただの文字に置き換えることです。 | < を < に |
| 入力値 | 利用者が画面から入れた文字です。 |
学生が入力するセルは、このプレビューでは空白です。
入力を SQL につなげると命令が変わる
検索ページは、入力された名前を SQL の文に入れてデータベースに問い合わせます。
入力をそのまま文字列としてつなげると、入力に混ぜた記号も SQL の一部になります。OR '1'='1' を足すと、条件がいつも成り立ち、全員の行が返ります。
これが SQL インジェクションです。守り方は、入力をプレースホルダーで値として別に渡すことです。
試す: つなげた場合と分けた場合
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE orders (customer TEXT, total INTEGER)")
db.execute("INSERT INTO orders VALUES ('yamada', 3800), ('suzuki', 12500)")
# 注文検索の欄に、だれかが入力した文字列
typed = "yamada' OR '1'='1"
# 悪い例: 入力をそのまま SQL の文につなげる
query = "SELECT * FROM orders WHERE customer = '" + typed + "'"
print(query)
print("つなげた場合:", db.execute(query).fetchall())
# 良い例: 入力は ? の場所に、値として別に渡す
safe = "SELECT * FROM orders WHERE customer = ?"
print("プレースホルダー:", db.execute(safe, (typed,)).fetchall())
# typed を "yamada" に変えて、もう一度実行してください。
投稿をそのまま表示するとスクリプトが動く
口コミや問い合わせのように、だれかの入力をページに表示する機能はよくあります。
投稿に <script> を混ぜると、そのまま表示した場合、読んだ人のブラウザでプログラムが動きます。ログイン中の情報を盗まれることもあります。これが XSS です。
守り方は、表示する前にエスケープすることです。記号がただの文字になり、プログラムとして動きません。
試す: そのままとエスケープ
import html
# ラーメン店の口コミ欄に投稿された文
review = "おいしい <script>alert('x')</script>"
# 悪い例: 投稿をそのままページに入れる
print("<p>" + review + "</p>")
# 良い例: 特別な記号を、ただの文字に置き換えてから入れる
print("<p>" + html.escape(review) + "</p>")
# review を "<b>また来ます</b>" に変えて、出力を比べてください。
よくある思い違い
注意
「画面の入力チェックがあるから安全」と考える方は多いです。画面のチェックは、ブラウザを使わずに送れば飛ばせます。サーバー側でプレースホルダーとエスケープを使って、初めて守れます。