ゆめさくセキュリティ

レッスン 2-1

SQL インジェクション

文字列をつなぐと、入力がそのまま SQL の命令として読まれます。

目安 60 分 トラック 2 Web アプリケーションの 1 本目

読み終えたらなぜ表や列の名前はプレースホルダで縛れないのかを説明できる

この頁の流れ

  1. 1なぜ通るのか
  2. 2脆いコードと直したコード
  3. 3ログでの見え方と検知
  4. 4直し方と確かめ方
  5. 5関わる事例
  6. 6確認クイズ
  7. 7説明課題
  8. 8一次資料
  9. 9次へ

入力を SQL の文字列につなぐと、値のはずの入力が命令として読まれます。プレースホルダが効く範囲と効かない範囲 (表や列の名前、並び順) を分け、記録での見え方と、直ったことを試験で確かめる方法までを扱います。

出典を確かめた日 2026年10月24日

なぜ通るのか

注文を名前で探す画面を作ったとします。サーバーは、入力された名前を SQL の文の中に埋め込んで、データベースに問い合わせます。

ここで、名前を文字列としてつなぎ合わせて SQL の文を作ると、データベースには「どこまでが命令で、どこからが値か」の区別が届きません。データベースが受け取るのは、つなぎ終わった 1 本の文字列だけだからです。

値と命令が混ざる所

SQL では、値の文字列を引用符で囲みます。入力に引用符が入っていると、そこで値が終わったと読まれ、その後ろは命令の続きとして解釈されます。

悪意が無くても起きます。たとえば「O'Brien」という名前で探すと、名前の中の引用符で値が途中で閉じ、文の形が崩れてエラーになります。エラーになるということは、入力が文の形を変えられるということです。形を変えられるなら、条件を付け足したり、別の表を読んだりする文にも変えられます。

プレースホルダが効く範囲

プレースホルダは、SQL の文の中に「ここに値が入る」という印を置き、値は文とは別に渡す仕組みです。データベースは先に文の形を決めてから値を当てはめるので、値の中に引用符があっても、文の形は変わりません。

IPA の「安全なウェブサイトの作り方」は、SQL の文の組み立てをすべてプレースホルダで行うことを根本的な解決に挙げています。文をデータベースの側で先に組む静的プレースホルダなら、原理として SQL インジェクションの余地がなくなるとも書いています。

効かない範囲

プレースホルダで渡せるのは値だけです。表の名前、列の名前、並び順の向き (ASC や DESC) は文の形そのものなので、値として渡せません。OWASP の Cheat Sheet は、こうした部分には許した候補からの選択か、問い合わせの作り直しで守るよう勧めています。

一覧の並び替えのように、利用者の選んだ列で並べる機能は、ここで穴が開きやすい所です。

脆いコードと直したコード

ラボの中の小さな関数で比べます。Python に付いている sqlite3 だけで動き、外とは通信しません。

値をつなぐか、値を渡すか

脆いコード名前を文字列でつなぐ
def find_orders(db, name):sql = ("SELECT id, item FROM orders"" WHERE name = '" + name + "'")return db.execute(sql).fetchall()
直したコード名前はプレースホルダで渡す
def find_orders(db, name):sql = ("SELECT id, item FROM orders"" WHERE name = ?")return db.execute(sql, (name,)).fetchall()

直した方では、文の形は WHERE name = ? で固定されています。名前は 2 つ目の引数で別に渡るので、引用符を含んでいても値のままです。

並び順は、許した候補から選ぶ

脆いコード列の名前をそのままつなぐ
def list_orders(db, sort):sql = ("SELECT id, item FROM orders"" ORDER BY " + sort)return db.execute(sql).fetchall()
直したコード候補の表から選ぶ
ORDER = {"new": "created_at DESC","old": "created_at ASC"} def list_orders(db, sort):by = ORDER.get(sort, ORDER["new"])sql = ("SELECT id, item FROM orders"" ORDER BY " + by)return db.execute(sql).fetchall()

直した方もつないではいますが、つなぐのはコードの中に書いた 2 つの候補だけです。利用者が送るのは「new」か「old」という鍵で、鍵に無い物は既定の並び順になります。

ログでの見え方と検知

脆いコードに「O'Brien」を渡すと、データベースは文の形が崩れたことをエラーで返します。sqlite3 では、記録に次のような行が残ります。

記録脆いコードのときの記録
sqlite3.OperationalError:near "Brien": syntax error

この行は、入力が文の形を変えたことの直接の証拠です。守る側は、次の 3 つを記録と見張りに入れます。

見る物なぜ効くか
SQL の構文エラーの数ふだんの利用では、構文エラーはほとんど出ない
エラーと一緒に残す要求の IDどの画面のどの入力で崩れたかを、あとから追える
画面に返すエラーの中身データベースの名前や文を返すと、攻め手の手がかりになる

IPA は、データベースのエラーの文をそのままブラウザに出さないことを、保険的な対策に挙げています。記録には詳しく残し、画面には「うまくいきませんでした」とだけ返す、という分け方です。

直し方と、直ったことの確かめ方

自分の PC の中だけで行います。外のサービスや、他人の機械には向けません。

  1. 作業用のフォルダに、注文の表を 1 つ持つ sqlite3 のデータベースを作り、「O'Brien」の注文を 1 件入れておく
  2. 脆い find_orders に「O'Brien」を渡し、構文エラーになることを記録で確かめる
  3. 関数を直したコードに置き換える
  4. 同じ「O'Brien」を渡し、エラーにならず、その人の注文 1 件だけが返ることを確かめる
  5. 下の試験を足し、直す前のコードに戻すと試験が落ちることも確かめる
  6. 終わったら、作業用のフォルダごと消す
Python直ったことを固める試験
def test_quote_in_name(db):rows = find_orders(db, "O'Brien")assert rows == [(3, "紅茶")] def test_unknown_sort_uses_default(db):assert (list_orders(db, "zzz")== list_orders(db, "new"))

試験は「同じ入力で、直す前は崩れ、直したあとは値として扱われる」ことを固定します。あとで誰かが文字列をつなぐ書き方に戻しても、ここで止まります。

あわせて、データベースに接続する利用者の権限を、その画面に要る分だけに絞ります。IPA はこれも保険的な対策に挙げています。穴が残っていても、読める表と書ける表が少なければ、被害は小さくなります。

関わる事例

ファイル転送の製品 MOVEit Transfer では、2023年5月に、インターネットに公開されたウェブアプリの SQL インジェクション (CVE-2023-34362) が悪用されました。CISA と FBI の注意喚起によると、悪用は修正が出る前から始まっていました。

CISA と FBI は 2024年3月25日に、SQL インジェクションをソフトウェアから無くすよう作り手に求める文書を出しています。この文書は、多くの組織に被害を出したファイル転送製品での悪用を受けて書かれたと述べています。

確認クイズ

1 問目

一覧の並び順を、利用者が選んだ列の名前で決めたい。どう作るのがよいですか。

答えだと思う選択肢を押すと、その場で正誤と理由が開きます。

2 問目

ふだんは出ない SQL の構文エラーが、ある画面で急に増えました。まず何を疑いますか。

答えだと思う選択肢を押すと、その場で正誤と理由が開きます。

説明課題

次のことを、自分の言葉で 3 行にまとめてください。書けたら、このレッスンは修了です。

  • なぜ表や列の名前はプレースホルダで縛れないのか
模範解答を見る

プレースホルダは、文の形を先に決め、あとから値だけを当てはめる仕組みだ。表や列の名前は値ではなく文の形そのものなので、あとから当てはめられない。だから名前は、コードに書いた候補から選ばせて守る。

一次資料

どれも 2026年10月24日に開いて、題と中身を確かめました。

次へ

次のレッスンは「クロスサイトスクリプティング」です。データベースではなくブラウザの中で、値のはずの入力が命令として読まれる穴を扱います。

次のレッスン 2-2 クロスサイトスクリプティング

このレッスンにつながる物