「SQLインジェクションって聞いたことはあるけど、何がどう危ないのか説明できない…」
そんな方、けっこう多いんじゃないでしょうか?
今回は、Webアプリを作るエンジニアが最低限おさえておきたいセキュリティの穴を9つ、実際に起きた事件と「なぜ起こるのか」をセットで、できるだけやさしく解説します。
- 脆弱性の多くは「データと命令が混ざる」「確認が抜ける」「古いまま放置」の3つから生まれる
- 実際に起きた事件と、危ないコード・安全なコードをセットで紹介
- 全部覚えなくてOK。最後に「最低限この3つ」をまとめています
- AIに書かせたコードをレビューするときのチェックリストとしても使えます
- そもそも脆弱性はなぜ生まれるの?
- ① SQLインジェクション|入力が「命令」になってしまう
- ② XSS(クロスサイトスクリプティング)|書き込みが「プログラム」として動く
- ③ 認証の不備|「あなたは本当に本人?」の確認が甘い
- ④ 認可の不備(IDOR)|URLの数字を変えたら他人のデータが見えた
- ⑤ CSRF|知らないうちに「自分の名前で」操作される
- ⑥ SSRF|サーバーが「内側」に勝手にアクセスしてしまう
- ⑦ WAFとは?|入口の「門番」。でも万能じゃない
- ⑧ 秘密情報の書き込み|鍵を表札に貼っておく
- ⑨ 古いライブラリとエラーの出しすぎ|自分で書いていないコードも自分の責任
- 全部覚えなくていい!最低限この3つだけは守ろう
- もっとちゃんと学びたい人へ
- まとめ
そもそも脆弱性はなぜ生まれるの?
いきなり個別の話に入る前に、ひとつだけ覚えておいてほしいことがあります。
Webアプリの脆弱性の多くは、次の3パターンのどれかから生まれます。
- データと命令が混ざる:ユーザーが入力した「ただの文字」が、プログラムの「命令」として実行されてしまう
- 確認が抜ける:「この人は本当に本人?」「この人にこの操作を許していい?」のチェックが足りない
- 古いまま放置する:修正版が出ているのに、ライブラリやソフトを更新していない
このあと紹介する9つも、だいたいこのどれかに当てはまります。
「あ、これは①のパターンだな」と思いながら読むと、ぐっと理解しやすくなりますよ。

仕組みさえわかれば、怖さもグッと減るね。
① SQLインジェクション|入力が「命令」になってしまう
たとえるなら:レストランの注文書の「備考欄」に「ついでにレジのお金も全部ください」と書いたら、店員さんがそのまま実行してしまう。そんなイメージです。
実際に起きたこと:2015年、イギリスの通信会社TalkTalkがSQLインジェクションで攻撃されました。買収した会社から引き継いだまま放置していた古いWebページ3つが入口になり、約15万7,000人分の個人情報(うち約1万5,600人分は銀行口座番号)が盗まれています。しかも、使っていたデータベースソフトの修正版は3年半以上前に出ていたのに、適用されていませんでした。英国の監督機関ICOは、当時として過去最高の40万ポンドの制裁金を科しています。
2023年には、ファイル転送ソフト「MOVEit Transfer」のSQLインジェクションの脆弱性が攻撃グループに悪用され、世界中の企業や政府機関から大量のデータが盗まれました。昔の話ではなく、今も現役の攻撃なんです。
なぜ起こるのか:ユーザーの入力を、SQL文に「文字列としてつなげて」しまうからです。
// 危ない例:入力をそのままSQLにつなげている
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = " . $id;
ここで id に 1 OR 1=1 と入力されると、SQLは「idが1、または 1=1(常に正しい)」という意味になり、全ユーザーのデータが返ってきます。ログイン画面なら、パスワード欄に ' OR '1'='1 と入れるだけでログインできてしまう、という古典的な手口もあります。
防ぎ方:「プレースホルダ(プリペアドステートメント)」を使います。SQLの形を先に決めておき、入力値はあとから「データとして」渡す方法です。こうすると、入力がどんな文字でも命令として解釈されません。
// 安全な例:PDOのプレースホルダを使う
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$_GET['id']]);
// WordPressなら $wpdb->prepare() を使う
$row = $wpdb->get_row(
$wpdb->prepare("SELECT * FROM {$wpdb->prefix}orders WHERE id = %d", $id)
);

「入力は信じない、SQLにはつなげない」。これだけで防げるのに、今でも被害が出続けてるんだよね。
② XSS(クロスサイトスクリプティング)|書き込みが「プログラム」として動く
たとえるなら:掲示板に貼った「メモ」が、それを読んだ人の画面で勝手に動き出す。読んだ人のブラウザで、攻撃者のプログラムが実行されてしまう問題です。
実際に起きたこと:2005年、当時大人気だったSNS「MySpace」で、あるユーザーが自分のプロフィールにスクリプトを仕込みました。プロフィールを見た人は自動で彼を「友達」に追加し、さらに同じスクリプトが自分のプロフィールにもコピーされる仕組みです。この「Samyワーム」は、20時間ほどで100万人以上に広がりました。
なぜ起こるのか:ユーザーが入力した文字を、画面に表示するときに「エスケープ(無害化)」していないからです。
// 危ない例:入力をそのまま表示している
echo "こんにちは、" . $_GET['name'] . "さん";
name に <script>…</script> を入れられると、ブラウザはそれを「ただの名前」ではなく「プログラム」として実行します。ログイン中のCookieを盗まれたり、偽のログインフォームを表示されたりします。
防ぎ方:画面に出すときに必ずエスケープします。< を < のように変換して、「タグ」ではなく「文字」として表示させるわけです。
// 安全な例:出力時にエスケープ
echo "こんにちは、" . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8') . "さん";
// WordPressなら esc_html() / esc_attr() / esc_url() を使い分ける
echo esc_html( $name );
あわせて、Cookieに HttpOnly を付ける(JavaScriptからCookieを読めなくする)、CSP(読み込んでいいスクリプトを制限する設定)を入れると、万一のときの被害を小さくできます。

「入力時」じゃなくて「出力時」にエスケープ、がポイントだね。
③ 認証の不備|「あなたは本当に本人?」の確認が甘い
ここで言葉を整理しておきます。
認証=「あなたは誰?」を確かめること(ログイン)
認可=「あなたは何をしていい?」を確かめること(権限チェック)
似ていますが、別物です。まずは認証から。
たとえるなら:社員証を見せれば誰でも通れる受付。しかもその社員証が、別の会社から盗まれた物の使い回しでも通ってしまう状態です。
実際に起きたこと:2019年7月、コンビニのスマホ決済「7pay」は、サービス開始直後に不正利用が発覚しました。被害は808人・約3,860万円。運営会社は、他サイトから漏れたIDとパスワードの組み合わせを使う「リスト型攻撃」が原因だと結論づけています。2段階認証がなかったことも強く批判され、7payはわずか3ヶ月でサービス終了になりました。
2023年には、遺伝子検査サービスの23andMeで、使い回しのパスワードを突かれて約1万4,000アカウントに侵入されました。そこから「親族を探す」機能をたどられ、最終的に約690万人分の情報が見られています。
なぜ起こるのか:
- パスワードだけで本人確認している(使い回されていたら一発でアウト)
- ログインを何回失敗しても止めない(総当たりやリスト型攻撃を受け放題)
- パスワードを平文や弱い方式で保存している(DBが漏れたら全員分バレる)
防ぎ方:2段階認証やパスキーを用意する、ログイン失敗が続いたら一時ロックする、パスワードは必ず専用の関数でハッシュ化して保存する、の3つです。
// パスワードの保存と照合(PHP標準の関数を使う)
$hash = password_hash($password, PASSWORD_DEFAULT); // 保存するのはハッシュだけ
if (password_verify($input, $hash)) {
// ログイン成功
}

自分でパスワードの暗号化を発明しちゃダメ。言語に用意されてる関数を使うのが一番安全だよ。
④ 認可の不備(IDOR)|URLの数字を変えたら他人のデータが見えた
たとえるなら:ホテルで自分の部屋のカードキーを使ったら、隣の部屋のドアも開いてしまった。そんな状態です。
実際に起きたこと:2019年、アメリカの不動産保険会社First American Financialで、書類を表示するURLの数字を1つ変えるだけで、他人の書類が見られることが発覚しました。対象は約8億8,500万件。銀行口座の明細、社会保障番号、運転免許証の画像などが、ログインすらせずに見られる状態だったんです。
なぜ起こるのか:「ログインしているか」は確認していても、「そのデータがその人のものか」を確認していないからです。IDが連番だと、数字を1つずつ変えるだけで全部見られてしまいます。
// 危ない例:IDだけで注文を取ってくる
$order = $db->find('orders', $_GET['order_id']);
// 安全な例:「ログイン中の本人の注文か」も条件に入れる
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ? AND user_id = ?');
$stmt->execute([$_GET['order_id'], $currentUser->id]);
IDをランダムな文字列(UUIDなど)にするのも助けにはなりますが、それだけでは不十分です。サーバー側で毎回「本人のデータか」をチェックするのが本筋です。

自動テストでも見つけにくいから、レビューで「この画面、他人のIDでも見られない?」って聞くクセをつけるといいよ。
⑤ CSRF|知らないうちに「自分の名前で」操作される
たとえるなら:あなたが寝ている間に、誰かがあなたのハンコを押した申込書を役所に出してしまう。役所は「ハンコが本物だから本人だ」と受け付けてしまいます。
実際に起きたこと:2012年、横浜市のWebサイトの投稿フォームに小学校への襲撃予告が書き込まれ、大学生が逮捕されました。ところが後になって、真犯人が「CSRFを使って書き込ませた」と名乗り出て、誤認逮捕だったことがわかります。大学生は、罠のリンクを踏んだだけで、自分のPCから書き込みを送らされていたんです。
なぜ起こるのか:ブラウザは、そのサイトのCookie(ログイン情報など)を自動で送ります。サーバー側が「この送信は、正規の画面から本人の意思で送られたものか」を確認していないと、罠サイトから送られたリクエストも受け付けてしまいます。
防ぎ方:フォームに「その画面でしか発行されない合言葉(CSRFトークン)」を埋め込み、送信時に照合します。WordPressなら、標準のnonceの仕組みがこれです。
// WordPress:フォームにnonceを埋め込む
wp_nonce_field( 'save_settings', 'my_nonce' );
// 受け取る側で照合する
if ( ! isset( $_POST['my_nonce'] ) || ! wp_verify_nonce( $_POST['my_nonce'], 'save_settings' ) ) {
wp_die( '不正なリクエストです' );
}
Cookieに SameSite=Lax 以上を付けておくと、他サイトからのリクエストにCookieが付きにくくなり、二重の守りになります。

「重要な操作はPOST+トークン」。フレームワークを使ってるなら、たいてい標準機能があるよ。
⑥ SSRF|サーバーが「内側」に勝手にアクセスしてしまう
たとえるなら:会社の受付係に「裏の倉庫から、この箱を取ってきて」と頼むと、相手が誰でも取ってきてしまう。受付係(サーバー)は社内(内部ネットワーク)に入れるので、外の人が直接入れない場所の物まで持ち出せてしまいます。
実際に起きたこと:2019年、アメリカの大手銀行Capital Oneで、約1億600万人分のクレジットカード申込データが盗まれました。入口になったのは、設定ミスのあったWAF(このあと解説します)です。攻撃者はWAFにSSRFを仕掛けて、AWSの「メタデータ」という内部の窓口から認証情報を取得。そのWAFに必要以上の権限が付いていたため、保存されていたデータまで読み出されてしまいました。
なぜ起こるのか:「指定したURLの画像を取り込む」「指定したURLのプレビューを表示する」といった機能で、サーバーがユーザー指定のURLに何でもアクセスしてしまうからです。そこに内部用のアドレスを指定されると、外からは見えないはずの情報が抜かれます。
防ぎ方:
- アクセスしてよいURLを「許可リスト」で決める
- 内部用のIPアドレス(127.0.0.1、10.x.x.x、169.254.169.254 など)へのアクセスを拒否する
- クラウドのメタデータは新しい方式(AWSならIMDSv2)を使う
- サーバーに付ける権限は最小限にする

Capital Oneは「SSRF」と「権限の付けすぎ」の合わせ技だったんだ。どっちか片方でも防げてたら、被害はかなり小さかったはずだね。
⑦ WAFとは?|入口の「門番」。でも万能じゃない
WAF(Web Application Firewall)は、Webアプリの手前に立つ門番です。「SQLインジェクションっぽい文字列」「XSSっぽいタグ」など、よくある攻撃のパターンを見つけたら、アプリに届く前に止めてくれます。
| WAFで防ぎやすいもの | WAFでは防ぎにくいもの |
|---|---|
| よくある形のSQLインジェクション・XSS | ④の認可の不備(正規のリクエストに見える) |
| 既知の脆弱性を狙った攻撃 | アプリのロジックのミス |
| 大量のアクセス・機械的な攻撃 | WAF自体の設定ミス(Capital Oneの例) |
つまりWAFは、コードの対策をすり抜けたときの「保険」です。WAFがあるからコード側は手を抜いていい、ということにはなりません。
レンタルサーバーでも、ConoHa WINGやエックスサーバーなどは管理画面からWAFをオンにできます。WordPressの運営者は、まず有効になっているかを確認しておきましょう。
▶ WordPressサイト運営のセキュリティ対策(WAF・PHP・プラグイン更新)

門番がいても、家の鍵はちゃんと閉めようねって話だね。
⑧ 秘密情報の書き込み|鍵を表札に貼っておく
たとえるなら:家の合鍵を、玄関の表札にテープで貼っておくようなものです。
実際に起きたこと:2022年、トヨタ自動車は、コネクティッドサービス「T-Connect」の利用者約29万6,000件のメールアドレスなどが漏えいした可能性があると発表しました。原因は、Webサイトの開発を委託していた会社が、ソースコードの一部を誤って「公開」設定でGitHubに上げていたこと。そのコードにデータサーバーへのアクセスキーが書かれていて、2017年12月から2022年9月まで、約5年間も誰でも見られる状態でした。
なぜ起こるのか:
- APIキーやパスワードを、ソースコードに直接書いている
- 非公開のつもりが、公開リポジトリにpushしてしまう
- Gitは履歴が残るので、あとからファイルを消しても過去のコミットに残っている
防ぎ方:秘密情報はコードに書かず、.env ファイルや環境変数に分けます。.env は必ず .gitignore に入れましょう。もし漏れたら、履歴を消すより先にそのキーを無効化して作り直すのが鉄則です。
# .gitignore
.env
*.pem
wp-config.php

「消したから大丈夫」は通用しないんだ。漏れた鍵は、もう鍵じゃないと思った方がいいね。
⑨ 古いライブラリとエラーの出しすぎ|自分で書いていないコードも自分の責任
古いライブラリ:2021年12月、Javaで広く使われているログ出力ライブラリ「Log4j」に、とんでもない脆弱性(通称Log4Shell)が見つかりました。特定の文字列をログに出すだけで、外部からサーバーを操れてしまうというものです。世界中のサービスが対応に追われました。
自分が1行も書いていないライブラリでも、使っている以上は自分のサービスの穴になります。TalkTalkの件も、修正版が出ていたのに更新していなかったことが原因の一つでした。
- GitHubのDependabotで、脆弱性のある依存ライブラリを自動で知らせてもらう
npm auditやcomposer auditを定期的に実行する- 「動いているから触らない」ではなく、更新を習慣にする
エラーの出しすぎ:たとえるなら、金庫破りに失敗した泥棒に「暗証番号は4桁で、2桁目が違います」と教えてしまうようなものです。SQLのエラーがそのまま画面に出ると、テーブル名やSQLの形がわかり、①のSQLインジェクションの手がかりになります。
// 本番環境ではエラーを画面に出さない
// php.ini
display_errors = Off
log_errors = On
// WordPress(wp-config.php)
define( 'WP_DEBUG', false );

エラーはログに残して、画面には「エラーが発生しました」だけ。ユーザーにも攻撃者にも、余計なことは教えないのが正解だよ。
全部覚えなくていい!最低限この3つだけは守ろう
最後に、9つをまとめておきます。
| 脆弱性 | ひとことで言うと | 対策 |
|---|---|---|
| SQLインジェクション | 入力がSQLの命令になる | プレースホルダ |
| XSS | 書き込みがプログラムとして動く | 出力時にエスケープ |
| 認証の不備 | 本人確認が甘い | 2段階認証・試行制限・ハッシュ保存 |
| 認可の不備(IDOR) | 他人のデータが見える | 毎回「本人のデータか」を確認 |
| CSRF | 本人の名前で勝手に操作される | CSRFトークン |
| SSRF | サーバーが内部に勝手にアクセス | 許可リスト・内部アドレス拒否 |
| WAF | 入口の門番(保険) | 有効化+設定確認 |
| 秘密情報の書き込み | 鍵をコードに書く | .env+.gitignore、漏れたら無効化 |
| 古いライブラリ・エラー表示 | 放置と教えすぎ | 定期更新・本番はエラー非表示 |
正直、これを全部いきなり完璧にするのは大変です。なので、まずはこの3つから始めてください。
- SQLは必ずプレースホルダで書く(文字列でつなげない)
- 画面に出すときは必ずエスケープする
- データを返す前に「この人のデータか」を必ず確認する
この3つは、AIにコードを書かせたときのレビューでもそのまま使えます。AIは動くコードを書くのは得意ですが、こうした確認を平気で抜かすことがあるので、人間の目で必ずチェックしましょう。

AIが書いたコードこそ、この3つを見るだけで事故がかなり減るよ。
もっとちゃんと学びたい人へ
体系的に学ぶなら、通称「徳丸本」と呼ばれる『体系的に学ぶ 安全なWebアプリケーションの作り方』が定番です。今回紹介した脆弱性が、なぜ起こるのかから対策まで、仕組みレベルで丁寧に解説されています。手元に1冊置いておくと安心ですよ。
無料で読める資料なら、IPA(情報処理推進機構)が公開している「安全なウェブサイトの作り方」もおすすめです。チェックリスト形式でまとまっているので、レビューのお供にちょうどいいです。
まとめ
今回は、エンジニアがコーディングで気を付けたいセキュリティの穴を9つ、実例つきで紹介しました。
- 脆弱性の多くは「データと命令が混ざる」「確認が抜ける」「古いまま放置」から生まれる
- どれも、実際に大きな被害が出ている「現役の攻撃」
- まずは「プレースホルダ・エスケープ・本人確認」の3つを徹底する
個人でできるセキュリティ対策は、こちらにまとめています。
▶ 個人情報の流出・乗っ取りを防ぐセキュリティ対策まとめ|エンジニアが優先度順に解説

「知ってる」と「書ける」は別物。次に書くコードから、ひとつずつ意識していこうね。


コメント