プログラマのための IT 教科書

Web の脆弱性を体系的に

この部の 2 / 8 章 ・ 全体で 63 / 76 章 ・ 読了目安 45 分

この章を読むとできるようになること
  • 脆弱性の分類を共通言語として使える
  • SSRF の危険な機能を見分けられる
  • 自分の PR をチェック観点で確認できる

前章では、入力を信じないという原則と、代表的な攻撃を扱いました。 この章では、実務で実際に指摘される脆弱性を、もう少し網羅的に見ます。

「全部覚える」必要はありません。存在を知っていることが目的です。 知らない脆弱性は、レビューでも設計でも検出できません。

OWASP Top 10 という共通言語

Web アプリのセキュリティには、OWASP Top 10 という広く使われる一覧があります。 数年ごとに更新され、実際の被害データに基づいて選ばれています。

アクセス制御の不備 / 暗号化の失敗 / インジェクション /
安全でない設計 / 設定ミス / 脆弱な依存 /
認証の不備 / データ整合性の不備 / ログと監視の不足 / SSRF

セキュリティ診断の報告書は、この分類で書かれてきます。 だから、この言葉を知っていると報告書が読めます。

https://owasp.org/www-project-top-ten/

アクセス制御の不備

最も被害が大きく、最も多い分類です。認証と認可の実装の IDOR もここに入ります。

権限チェックの漏れ

□ URL の ID を書き換えると他人のデータが見える(IDOR)
□ 管理者用の API に、一般ユーザーがアクセスできる
□ 画面には出ないが、API を直接叩けば実行できる
□ 別テナントのデータが見える(マルチテナント)

新しいエンドポイントを追加するたびに、人間が忘れずに書くのは不可能です。 認証と認可の実装のとおり、既定で拒否し、明示的に許可する構造にします。

マスアサインメント

// 危険: リクエストのJSONを、そのままモデルに流し込む
var user User
json.NewDecoder(r.Body).Decode(&user)
db.Save(&user)

利用者が {"name":"sato","role":"admin","is_verified":true} を送れば、 そのまま権限が昇格します。

// 受け取ってよい項目だけを、明示的に定義する
type UpdateProfileRequest struct {
    Name string `json:"name"`
    Bio  string `json:"bio"`
}

入力用の型と、DB のモデルを分ける——設計の基礎の設計の話でもあります。

「画面に出していないから安全」ではない
□ 管理メニューを非表示にした → API は生きている
□ フォームに項目を出していない → リクエストには足せる
□ 無効化したボタン → HTML を書き換えれば押せる

クライアント側の制御は、すべて利用者が変更できます(フロントエンドの実務)。 判定は必ずサーバー側です。

SSRF — サーバーに代わりに通信させる

利用者「この URL の画像を取り込んで」
   → サーバーがその URL にアクセスする
   → 利用者が `http://169.254.169.254/...` を指定したら?

クラウドのメタデータサーバーには、 そのインスタンスの認証情報が入っていることがあります。 外部からは届きませんが、サーバー自身からは届きます。

□ URL を利用者から受け取って取得する機能は、すべて対象
   (画像取り込み、Webhook 登録、PDF 生成、プレビュー、インポート)
□ 許可リスト方式にする(このドメインだけ)
□ プライベート IP・ループバック・メタデータへのアクセスを禁止する
□ リダイレクトを追わない(追うなら再検証する)
□ 名前解決の結果を検証する(DNS で内部 IP に解決される攻撃がある)
□ 可能なら、外部通信専用の経路(プロキシ)を通す

ネットワークの基礎のプライベート IP の知識が、ここで効いてきます。

パストラバーサル

GET /files?name=../../etc/passwd
□ 利用者の入力をファイルパスに使わない(ファイルとストレージ)
□ どうしても必要なら、識別子から**サーバー側でパスを決める**
□ 正規化してから、許可されたディレクトリ配下かを検証する

オープンリダイレクト

https://example.com/login?next=https://attacker.example/fake-login
→ ログイン後、攻撃者のサイトに飛ばされる
→ 本物のドメインを経由しているので、利用者は信用してしまう
□ リダイレクト先は**自サイト内のパスだけ**を許可する
□ 外部 URL を許可する必要があるなら、許可リストで
□ 「//attacker.example」のような相対 URL に見える形にも注意する

フィッシングの踏み台として使われるため、診断では必ず指摘されます。

クリックジャッキング

攻撃者のページが、あなたのサイトを透明な iframe で重ねる
→ 利用者は「無料で見る」ボタンを押したつもりが、
   実際には「送金する」ボタンを押している
Content-Security-Policy: frame-ancestors 'none'

埋め込みを許可する必要がないなら、禁止するのが正解です。

セキュリティヘッダ

設定するだけで効く、費用対効果の高い防御です。

ヘッダ効果
Content-Security-Policy実行できるスクリプトの出所を制限(XSS の被害を減らす)
Strict-Transport-Security常に HTTPS を使わせる
X-Content-Type-Options: nosniff中身を推測して勝手に実行させない
frame-ancestorsクリックジャッキング対策
Referrer-Policy遷移先に URL を漏らさない
CSP は「保険」として効く

CSP を入れても、XSS が無くなるわけではありません。

しかし、XSS があった時に「実行できない」ようにできます。

Content-Security-Policy: default-src 'self'; script-src 'self'
→ 自サイト以外のスクリプトは実行されない
→ 外部への情報送信も制限できる

AI とセキュリティの「AI の出力から外部リソースを読み込ませる攻撃」も、 CSP で被害を抑えられます。

導入は段階的に行えます(Content-Security-Policy-Report-Only で 違反を検知だけする)。

レースコンディション

見つけにくいバグの「チェックしてから使う」が、攻撃として使われます。

残高1000円のユーザーが、1000円の購入リクエストを同時に10本送る
→ 全部が「残高チェック」を通過する
→ 10回購入が成立し、残高がマイナスになる
□ クーポンの多重利用
□ 招待コードの使い回し
□ 残高・ポイントの二重消費
□ 在庫の超過販売(間違えられない処理を作る)

攻撃者は意図的に同時実行を試みます。 「同時に来ることは滅多にない」は、防御になりません。

-- 条件付き更新で、原子的に判定する(SQL の書き方)
UPDATE wallets SET balance = balance - 1000
WHERE user_id = $1 AND balance >= 1000;
-- 更新件数 0 なら残高不足

安全でないデシリアライゼーション

外部から受け取ったデータを、そのままオブジェクトに復元しない。

□ 言語標準のシリアライズ形式(Java の Serializable、Python の pickle など)を
   外部入力に使わない → 任意コード実行につながる
□ JSON + スキーマ検証を使う(TypeScript の型・AI を製品に組み込む)

ログと監視の不足

これも脆弱性の分類に入っています。

□ 攻撃されたことに気づけない
□ 侵入後、何をされたか追跡できない(エンジニアと法律の報告義務にも関わる)
□ 認証失敗が何千回あっても、誰も気づかない
□ 認証の成功・失敗、権限エラー、重要な操作をログに残す
□ 異常な回数のアクセスにアラートを出す
□ ただし**ログに秘密情報を出さない**(セキュリティ)

レート制限

□ ログイン試行(総当たり攻撃)
□ パスワードリセット・SMS 送信(費用が発生する)
□ 検索・重い API(DoS)
□ AI 機能(コストの暴走。AI とセキュリティ)

「認証があるから大丈夫」ではありません。 認証前のエンドポイントほど、制限が必要です。

設定ミス

診断で指摘される定番です。

□ デバッグモードが本番で有効(スタックトレースが利用者に見える)
□ 管理画面が公開されている
□ 既定の認証情報のまま
□ 不要なポートが開いている
□ クラウドストレージが公開設定(ファイルとストレージ)
□ エラーメッセージが詳細すぎる(内部構造やSQLが漏れる)
エラーメッセージの情報量
✕ 「ユーザー user@example.com は存在しません」   → アカウント列挙(認証と認可の実装)
✕ 「SQL error: no such column: users.password」 → 内部構造が漏れる
○ 「メールアドレスまたはパスワードが正しくありません」

利用者には最小限、ログには詳細を——これが原則です。

自分の変更をチェックする観点

新人が PR を出す前に確認できる項目です。

□ 新しいエンドポイントに、権限チェックはあるか
□ ID を受け取っているなら、それが本人のものか確認しているか
□ 利用者の入力が、SQL・HTML・コマンド・パス・URL に渡っていないか
□ 受け取る項目を、明示的に定義しているか(マスアサインメント)
□ エラーメッセージに、内部情報が出ていないか
□ ログに秘密情報が出ていないか
□ 同時に2回実行されたらどうなるか

ユーザーが指定した URL の画像を取り込む機能を実装します。セキュリティ上、最も注意すべきことは?

実務の落とし穴まとめ

  1. 画面で隠して権限制御したつもり — API は直接叩ける
  2. リクエストをそのままモデルに流し込む — 権限を昇格できる
  3. URL を受け取って取得する機能に制限が無い — SSRF
  4. リダイレクト先を検証しない — フィッシングの踏み台
  5. セキュリティヘッダを設定していない — 設定するだけで効く防御を捨てている
  6. 同時実行を考慮しない — 残高・クーポンの二重利用
  7. レート制限が無い — 総当たり、費用の発生、DoS
  8. デバッグモードや詳細エラーが本番で有効 — 内部構造が漏れる
  9. 監視していない — 攻撃に気づけず、後追いもできない

まとめ

  • 知らない脆弱性は検出できない。網羅的な一覧(OWASP Top 10)を一度通す
  • 最も多く、被害が大きいのはアクセス制御の不備。既定で拒否する
  • 受け取る項目を明示的に定義する(マスアサインメント)
  • URL を受け取る機能は SSRF を疑う。メタデータサーバーが最大の標的
  • セキュリティヘッダと CSP は、コストが低く効果が高い
  • レースコンディションは攻撃手段。「滅多に起きない」は防御ではない
  • ログと監視の不足も脆弱性。気づけないことが被害を拡大する
  • エラーメッセージは、利用者には最小限、ログには詳細

公式ドキュメント・参考資料

対象リンク
OWASP Top 10https://owasp.org/www-project-top-ten/
OWASP チートシート集https://cheatsheetseries.owasp.org/
IPA「安全なウェブサイトの作り方」https://www.ipa.go.jp/security/vuln/websecurity/
MDN セキュリティhttps://developer.mozilla.org/ja/docs/Web/Security

OWASP チートシートは、実装時に引く形で使えます (「Node.js のセキュリティ」「JWT の扱い」など、テーマ別に整理されています)。 IPA の資料は日本語で読めるので、最初の1冊として適しています。

章末問題

プロフィール更新 API で、リクエストの JSON をそのまま User モデルにマッピングして保存しています。何が問題ですか。

次の章では、これらの防御の土台にある暗号を扱います。

読み終わったら記録しておくと、目次で進み具合が分かります。