背景:なぜ脅威モデリングが深夜2時のあなたを救うのか
深夜にサーバーがSSHブルートフォース攻撃を受けたとき、私は不快な事実に気づいた。アプリの開発に何週間もかけていたのに、誰がどうやって攻撃してくるかを考える時間は1時間にも満たなかった。侵害は巧妙なものではなかった――予測可能だったのだ。1ヶ月前に正しい問いを立てていれば、防げていた。
脅威モデリングとは、コードを書く前に、そうした問いを体系的に立てる実践だ。コンプライアンスのためのチェックボックスでも、四半期に一度の儀式でもない。まだ変更コストが低い段階で攻撃者のように考えることを強制する、設計上の規律である。
多くのチームはこれを抽象的に聞こえるという理由でスキップする。それは誤った判断だ。IBMのSystem Science Instituteの調査によると、本番環境でセキュリティ上の欠陥を修正するコストは、設計段階で発見する場合の15倍にもなる。STRIDEは、その欠陥をポストモーテムではなくホワイトボード上で見つけるための具体的な語彙を与えてくれる。
STRIDEが実際に意味するもの
STRIDEは1999年にMicrosoftで開発された脅威分類モデルだ。各文字が攻撃の種類に対応している:
- S — なりすまし(Spoofing):他者を装う行為(JWTトークンの偽造、APIの不正呼び出し)
- T — 改ざん(Tampering):転送中または保存中のデータを変更する(SQLインジェクション、リクエストボディの操作)
- R — 否認(Repudiation):実行した行為を否定する(監査ログの欠如、リクエスト署名なし)
- I — 情報漏洩(Information Disclosure):機密データの漏出(詳細なエラーメッセージ、暗号化されていないDBフィールド)
- D — サービス妨害(Denial of Service):可用性の妨害(上限のないAPIエンドポイント、リソース枯渇)
- E — 権限昇格(Elevation of Privilege):不正なアクセスレベルの取得(不適切なRBAC、IDOR脆弱性)
頭字語を暗記するだけでは不十分だ。システム図のすべてのコンポーネントを問い質すためのチェックリストとして使うこと。各トラストバウンダリ、各データフロー、各プロセスに対して――どのSTRIDEカテゴリが当てはまるかを問う。それがこの手法の全てだ。
脅威モデリングツールキットのセットアップ
高価なライセンスは必須ではない。実際に日々使われているものを紹介する:
オプション1:OWASP Threat Dragon(無料、ブラウザベース)
Threat Dragonを使うと、データフロー図(DFD)を描き、コンポーネントごとに脅威を注記できる。Dockerで2分以内にローカル起動できる:
docker pull owasp/threat-dragon:latest
docker run -it --rm \
-p 3000:3000 \
-e ENCRYPTION_JWT_REFRESH_SIGNING_KEY='your-secret-key' \
-e ENCRYPTION_JWT_SIGNING_KEY='your-signing-key' \
-e ENCRYPTION_KEYS='{"current":{"issuer":"tdServer","keys":[{"isPrimary":true,"kty":"oct","use":"enc","alg":"A256GCM","kid":"k1","k":"your-base64-key"}]}}' \
owasp/threat-dragon:latest
http://localhost:3000 にアクセスして、アーキテクチャを描き始めよう。アカウント不要、SaaSなし、ベンダーロックインなし。
オプション2:Python + pytmによる脅威モデリング
pytmを使うと、アーキテクチャをPythonで定義し、DFDと脅威レポートを自動生成できる。最大の利点は、脅威モデルがバージョン管理に入り、対象のコードと並んで管理できる点だ。
pip install pytm
from pytm import TM, Server, Datastore, Dataflow, Boundary, Actor
tm = TM("Web API Threat Model")
tm.description = "REST APIバックエンドのSTRIDE分析"
tm.isOrdered = True
internet = Boundary("Internet")
app_boundary = Boundary("Application Server")
db_boundary = Boundary("Database Layer")
user = Actor("User", inBoundary=internet)
api = Server("REST API", inBoundary=app_boundary)
auth = Server("Auth Service", inBoundary=app_boundary)
db = Datastore("PostgreSQL", inBoundary=db_boundary)
Dataflow(user, api, "HTTPSリクエスト")
Dataflow(api, auth, "トークン検証")
Dataflow(api, db, "SQLクエリ")
Dataflow(db, api, "クエリ結果")
Dataflow(api, user, "HTTPSレスポンス")
tm.process()
以下のコマンドで実行する:
python threat_model.py --report report.html
python threat_model.py --dfd # DFD図を生成する
出力は各コンポーネントの自動生成された脅威をSTRIDEカテゴリにマッピングする。人間の判断を代替するものではないが、明らかなギャップを検出し、CIパイプラインに直接組み込める。
実際のWeb APIアーキテクチャへのSTRIDEの適用
典型的な構成を考えてみよう:ユーザー向けREST API、PostgreSQLバックエンド、JWT認証、サードパーティ決済連携。各トラストバウンダリをSTRIDEで精査すると、3つの層すべてで想定外の問題が見つかる。
トラストバウンダリ:インターネット → APIゲートウェイ
システム内のどの箇所よりも多くのSTRIDEカテゴリが該当する:
- なりすまし:攻撃者が
X-Forwarded-Forヘッダーを偽造してIPレート制限を回避できるか? APIがそのヘッダーを無条件に信頼している場合、答えはYesだ。対策:送信元IPがロードバランサーのCIDR範囲内にある場合にのみ信頼する。 - サービス妨害:
/registerや/forgot-passwordのような未認証エンドポイントにレート制限はあるか? なければ、どちらも無料のDoSベクターになる。月5ドルの攻撃者が数分でDBコネクションプールを枯渇させられる。 - 改ざん:リクエストボディは処理前にスキーマに対して検証されているか? 登録ペイロードに検証なしで
role: "admin"フィールドが含まれていたことで、本番環境で多くのチームが痛い目を見てきた。
# 例:Pydanticによる厳格なリクエストバリデーション
from pydantic import BaseModel, field_validator
from typing import Literal
class RegisterRequest(BaseModel):
email: str
password: str
# クライアントからroleを受け取らない――サーバー側で割り当てる
@field_validator('email')
@classmethod
def email_must_be_valid(cls, v):
if '@' not in v:
raise ValueError('メールアドレスが無効です')
return v.lower().strip()
トラストバウンダリ:API → 認証サービス
- なりすまし:JWTの署名アルゴリズムは
RS256またはHS256に固定されているか? 古典的なalg: none攻撃は、デフォルトで許可するライブラリでは今も有効だ――そして2023年のCVEデータベースにもこの問題の記録がある。 - 否認:トークン発行時に
user_id、ip、issued_atをサーバー側でログに記録しているか? これを省略すると、セッションが侵害された際に何が起きたかを再現できない。 - 情報漏洩:認証エラーが「ユーザーが見つからない」と「パスワードが違う」を区別しているか? どちらも同じ汎用メッセージを返すべきだ。ユーザー列挙はPython約10行で自動化できる。
import jwt
# 許可するアルゴリズムを必ず明示的に指定する
def verify_token(token: str, public_key: str) -> dict:
try:
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"], # ["RS256", "none"] は絶対に使わない
options={"require": ["exp", "iat", "sub"]}
)
return payload
except jwt.ExpiredSignatureError:
raise ValueError("トークンの有効期限が切れています")
except jwt.InvalidTokenError:
raise ValueError("無効なトークンです") # 汎用メッセージ、詳細は漏らさない
トラストバウンダリ:API → データベース
- 改ざん:すべてのクエリはパラメータ化されているか? SQLでの生の文字列フォーマットは絶対に許容されない――2024年においても、内部ツールであっても、いかなる場合も。
- 権限昇格:アプリのDBユーザーに
DROP TABLE権限があるか? あってはならない。最小権限のロールにすることで、認証情報が漏洩した際の被害範囲を限定できる。 - 情報漏洩:PII項目(メール、電話番号、マイナンバー等)はディスクレベルだけでなく、カラムレベルで暗号化されているか? フルディスク暗号化は、侵害されたアプリプロセスが平文でクエリするデータを保護しない。
-- 最小権限のアプリケーションロールを作成する
CREATE ROLE api_user WITH LOGIN PASSWORD 'strong-random-password';
GRANT SELECT, INSERT, UPDATE ON users, sessions TO api_user;
GRANT SELECT ON products TO api_user;
-- DELETE、DROP、スキーマ変更は付与しない
検証と脅威モデルの継続的な維持
Confluenceのページにしか存在しない脅威モデルはすでに死んでいる。アーキテクチャは変化する。新しいマイクロサービスが追加される。サードパーティの連携が入れ替わる。モデルもそれに合わせて更新し続ける必要がある。
脅威モデルレビューチェックリスト
新しい機能、エンドポイント、または連携をリリースするたびに実施する――所要時間は約5分:
- 新しいトラストバウンダリを追加したか?(新しいマイクロサービス、新しいサードパーティAPI)
- 新しいデータフローにPIIや認証情報が含まれるか?
- 新しいエンドポイントは認証・認可チェックの対象になっているか?
- 新たに未認証の攻撃面を追加していないか?
- 新しいコンポーネントにレート制限があるか?
- 新しいコンポーネントのエラーレスポンスは内部情報を漏らさない程度に汎用的か?
CI/CDへの脅威モデリングの統合
pytmを使えば、プルリクエストごとに脅威レポートを生成して差分を確認できる。新たな脅威が検出されるとレビューが完了するまでマージがブロックされる:
# CIパイプライン内(GitHub Actionsの例)
- name: 脅威モデルレポートを生成する
run: |
pip install pytm
python threat_model.py --report threat_report_new.html
diff threat_report_baseline.html threat_report_new.html > threat_diff.txt
if [ -s threat_diff.txt ]; then
echo "脅威モデルが変更されました――レビューが必要です"
cat threat_diff.txt
exit 1
fi
セキュリティエンジニアがすべてのPRを監視する必要はない。パイプラインが自動的に変更を検知する。レビューが必要なタイミング――コードレビュー時に――行われる。6ヶ月後のポストモーテムではなく。
実践的な優先付け:脅威のリスク評価
特定されたすべての脅威を今のスプリントで対応する必要はない。2つの軸でそれぞれスコアリングする:
- 発生可能性:1(低い)〜 3(高い)
- 影響度:1(軽微)〜 3(致命的)
- リスクスコア:発生可能性 × 影響度
スコア6〜9:今のスプリントにセキュリティタスクとして追加。スコア3〜5:期限付きでバックログに追加。スコア1〜2:記録・受け入れ済みとして、翌四半期に再確認。これにより、あらゆる理論上の脅威をリリースを止めるP0ブロッカーとして扱うことなく、チームが前進し続けられる。
すべてを変える1つの習慣
すべての機能設計の最初に、30分の脅威モデリングセッションを設けること。コードを書く前に。ホワイトボードとSTRIDEチェックリストだけで十分だ。各トラストバウンダリで何が問題になりうるかを問う。脅威を書き出す。緩和策を割り当てる。以上だ。
SSHブルートフォースのインシデントが教えてくれたのは、攻撃者は都合のいいタイミングを待たないということだ。オンコールエンジニアが寝ていて、誰もダッシュボードを見ていない金曜の深夜に攻撃してくる。脅威モデリングとは、そうした会話を業務時間中に行うことだ――修正に1週間ではなく半日しかかからないうちに。

