OWASP Top 10とは?
OWASP Top 10は、Webアプリケーションにおける代表的なセキュリティリスクをまとめたものです。
2025年版では、以下の10項目が挙げられています。
- Broken Access Control(アクセス制御の不備)
- Security Misconfiguration(セキュリティ設定の不備)
- Software Supply Chain Failures(ソフトウェアサプライチェーンの不備)
- Cryptographic Failures(暗号化の失敗)
- Injection(インジェクション)
- Insecure Design(安全でない設計)
- Authentication Failures(認証の失敗)
- Software or Data Integrity Failures(ソフトウェアやデータの整合性の問題)
- Security Logging & Alerting Failures(セキュリティログ・アラートの失敗)
- Mishandling of Exceptional Conditions(例外的な状況の不適切な処理)
以下、それぞれについて「開発するときに何を意識すればいいのか」を見ていきます。
2021年版から何が変わったのか?
2025年版は、2025年11月にリリース候補が公開され、2026年1月に最終版が確定しました。
2021年版で覚えている人も多いと思うので、先に対応表を載せておきます。
| 2025 | カテゴリ | 2021 | 変化 |
|---|---|---|---|
| A01 | Broken Access Control | A01 | → 変わらず(SSRFを吸収) |
| A02 | Security Misconfiguration | A05 | ⬆️ 3ランク上昇 |
| A03 | Software Supply Chain Failures | ― | 🆕 新規 |
| A04 | Cryptographic Failures | A02 | ⬇️ 2ランク下降 |
| A05 | Injection | A03 | ⬇️ 2ランク下降 |
| A06 | Insecure Design | A04 | ⬇️ 2ランク下降 |
| A07 | Authentication Failures | A07 | → 変わらず(改名) |
| A08 | Software or Data Integrity Failures | A08 | → 変わらず |
| A09 | Security Logging & Alerting Failures | A09 | → 変わらず(Monitoring → Alerting) |
| A10 | Mishandling of Exceptional Conditions | ― | 🆕 新規 |
2021年版にあって2025年版で見当たらないものが2つあります。
- A10 SSRF → Broken Access Controlに吸収されました
- A06 脆弱で古くなったコンポーネント → Software Supply Chain Failuresに含まれる形になりました
SSRFについては「なくなった」わけではありません。
サーバに任意の宛先へリクエストさせられるということは、到達してはいけない場所に到達できているということなので、アクセス制御の問題として扱われるようになった、という整理です。
1. Broken Access Control(アクセス制御の不備)
何が問題なのか?
Broken Access Controlは、ユーザーが本来アクセスできない情報や機能にアクセスできてしまう問題です。
例えば、
GET /api/users/100
というAPIがあったとします。
ログインしているユーザーが自分のIDである100の情報を見ることは問題ありません。
しかし、
GET /api/users/101
とURLを書き換えただけで、別ユーザーの情報まで取得できてしまったらどうでしょうか。
これはアクセス制御の問題です。
ちなみにこの問題には名前がついていて、IDOR(Insecure Direct Object Reference)、API文脈では BOLA(Broken Object Level Authorization) と呼ばれます。
名前を知っておくと調べ物がかなり楽になるので、覚えておいて損はありません。
開発時に意識すること
「ログインしているか」だけではなく、
「このユーザーは、このリソースにアクセスしていいのか?」
まで確認する必要があります。
図にすると、こういう違いです。
ポイントは、認可のチェックをデータ取得と同じ場所に置くことです。
入口のコントローラだけで確認していると、別の経路から同じ処理が呼ばれたときに素通りします。
また、書き換えられるのはURLのIDだけではありません。
| 書き換えられる場所 | 例 |
|---|---|
| パスパラメータ | /api/users/101 |
| リクエストボディ | { "organizationId": 2 } |
| HTTPメソッド |
GETしか想定していないのにDELETEが通る |
| GraphQLのクエリ | 画面が要求していないフィールドを追加する |
特にGraphQLは注意が必要です。画面に表示していないフィールドでも、スキーマに定義されていればクエリで要求できてしまいます。
フロントエンドが表示していないことは、取得できないことを意味しません。
よくある誤解:連番IDをUUIDにすれば安全?
これはよく見かけますが、対策にはなりません。
推測しにくくなっただけで、認可のチェックは1つも増えていないからです。
UUIDはログ、共有URL、他のAPIのレスポンスなど、いろいろな場所から漏れます。1つ漏れれば、そのリソースは誰でも取得できます。
推測困難にすることと、権限を確認することは別の話です。
開発時のチェックポイント
- ログインしているか?
- このユーザーにこの操作を許可していいか?
- URLのIDを書き換えられても問題ないか?
- 一般ユーザーが管理者向けAPIを呼び出せないか?
- 認可のチェックを、データ取得と同じ場所に書いているか?
- 権限なしと存在しないで、レスポンスが変わっていないか?
最後の項目は見落としがちです。権限がない場合に403を返すと、「そのIDのリソースは存在する」ことが分かってしまいます。404に統一するのが無難です。
2. Security Misconfiguration(セキュリティ設定の不備)
何が問題なのか?
Security Misconfigurationは、セキュリティ上適切ではない設定によって脆弱性が発生する問題です。
例えば本番環境でDebugモードが有効になっていたり、不要なポートやサービスが公開されていたりするケースです。
分かりやすい例が.gitディレクトリの公開です。
curl https://example.com/.git/HEAD
これでref: refs/heads/mainのような応答が返ってきたら、ソースコードが復元できてしまう可能性があります。
.gitの中にはコミット履歴がまるごと入っているので、過去に間違えてコミットしてしまった認証情報まで一緒に出てきます。
原因はだいたい同じで、ビルド成果物ではなくリポジトリのワーキングツリーをそのままデプロイしていることです。
よくある誤解:除外設定を書けば大丈夫?
.gitignoreや--excludeで危険なものを除外していく方式は、必ずどこかで漏れます。
| 除外リスト方式 | 許可リスト方式 | |
|---|---|---|
| 考え方 | 危険なものを取り除く | 必要なものだけを含める |
| 新しいファイルが増えたとき | デフォルトで公開される | デフォルトで含まれない |
| 更新の手間 | 危険が増えるたびに追記 | 不要 |
| 漏れたときの結果 |
.envや.gitが公開 |
動かないので気づく |
除外リストは「更新を忘れた瞬間に危険側へ倒れる」設計です。
**「危険なものを除外する」のではなく、「必要なものだけを含める」**という方向で組み立てるほうが安全です。
開発時のチェックポイント
- 本番環境でDebugモードが有効になっていないか?
- 不要なポートを公開していないか?
- デフォルトパスワードを使用していないか?
- 不要な機能やサービスを有効にしていないか?
- エラーメッセージから内部情報が漏れていないか?
- デプロイ対象を「含めるもの」で定義しているか?
- ストレージ(S3など)が意図せず公開設定になっていないか?
このカテゴリが2025年版でA05からA02へ上がったのは、何もしなければ危険側になる設定が多いからだと考えると腑に落ちます。
ミスをしなくても危ない、というのがこのカテゴリの厄介なところです。
3. Software Supply Chain Failures(ソフトウェアサプライチェーンの不備)
何が問題なのか?
アプリケーションは、自分たちが書いたコードだけで構成されているわけではありません。
npm、pip、composer、Mavenなどから大量の外部パッケージを利用します。
図にすると、本番環境に届くまでの経路はこれだけあります。
この経路のどこか1つが侵害されると、悪意のあるコードが本番環境まで届きます。
2025年版で新しく追加されたカテゴリで、コミュニティ調査では1位でした。
自分たちが書いたコードは全体のごく一部で、残りはnode_modulesの中身、というプロジェクトは珍しくないと思います。
開発時に意識すること
見るべきなのは、直接依存だけではありません。
package.jsonに書いた10個のパッケージが、その先で数百のパッケージに依存している、というのが普通です。問題が起きるのはたいてい、自分で書いた覚えのない依存の側です。
もう一つ見落としやすいのがCI/CDです。
# タグは後から差し替えられる
uses: some-org/some-action@v1
# コミットSHAで固定する
uses: some-org/some-action@a1b2c3d4e5f6...
GitHub Actionsのサードパーティ製Actionをタグで指定していると、タグが指す先を差し替えられた場合に気づけません。
開発時のチェックポイント
- 使用しているライブラリに既知の脆弱性がないか?
- 不要な依存パッケージを追加していないか?
- パッケージのバージョンを適切に管理しているか?
- CI/CDで依存関係をチェックできているか?
- lockファイルをコミットしているか?
- 外部Actionをコミットハッシュで固定しているか?
4. Cryptographic Failures(暗号化の失敗)
何が問題なのか?
Cryptographic Failuresは、暗号化や機密情報の保護が適切に行われていないことによって発生する問題です。
例えばパスワードを平文で保存したり、HTTPで機密情報を送信したりするケースです。
ただ実際に多いのは、暗号アルゴリズムの選択ミスよりも、鍵がそのまま置いてあるケースだと思います。
Next.jsを使っているなら、まずここを確認してください。
NEXT_PUBLIC_は「公開してよい値」という意味の接頭辞です。
サーバでしか使わないつもりの値にこれを付けてしまうと、その時点で公開情報になります。
同じことが、フロントエンドから外部APIを直接叩く実装でも起きます。外部サービスを呼ぶなら、自前のバックエンドを1枚挟むのが基本です。
よくある誤解:ハッシュ化すれば安全?
パスワードの保存には、専用のアルゴリズムを使う必要があります。
| アルゴリズム | 設計思想 | パスワード保存に |
|---|---|---|
| MD5 / SHA-1 | 高速 | ❌ 衝突も既知 |
| SHA-256 | 高速 | ❌ 速すぎて総当たりに弱い |
| bcrypt | 意図的に遅い | ⭕️ |
| Argon2id | 意図的に遅い+メモリを消費 | ⭕️ 推奨 |
ハッシュ関数は本来「速いことが正義」ですが、パスワード保存だけは逆です。
遅いことが安全性になる、という点が他の用途と違います。
開発時のチェックポイント
- パスワードを平文保存していないか?
- パスワードのハッシュに専用アルゴリズムを使っているか?
- 通信をHTTPSで保護しているか?
- APIキーや秘密鍵をコードに直接書いていないか?
- クライアント側に置いてはいけない値を渡していないか?
- ログに機密情報を出していないか?
5. Injection(インジェクション)
何が問題なのか?
Injectionは、ユーザーから入力されたデータが、意図しない命令として解釈されてしまう問題です。
代表的なのがSQL Injectionです。
query = f"SELECT * FROM users WHERE id = {user_id}"
のように、ユーザー入力を直接SQLへ組み込むのは危険です。
2021年版のA03からA05に下がっていますが、これは問題が減ったというより、ORMやプレースホルダの利用が当たり前になり、フレームワークが守ってくれる範囲が広がったためです。
よくある誤解:ORMを使っていれば安全?
ORMが守ってくれるのは、値をバインドする部分だけです。
// Laravel でも、生SQLに変数を連結すれば普通に刺さる
User::whereRaw("name = '{$request->name}'")->get();
// カラム名を動的に組み立てる場合もバインドできない
User::orderBy($request->sort)->get();
バインドできる場所とできない場所を整理すると、こうなります。
| 場所 | プレースホルダ | 対策 |
|---|---|---|
| WHERE句の値 | ⭕️ | バインドする |
| INSERTする値 | ⭕️ | バインドする |
| テーブル名 | ❌ | 許可リストで検証 |
| カラム名 | ❌ | 許可リストで検証 |
| ソート方向(ASC/DESC) | ❌ | 許可リストで検証 |
| LIMIT / OFFSET | ❌ | 数値にキャストする |
「ORMを使っているから大丈夫」ではなく、バインドできない箇所だけを洗い出すという考え方になります。
対策の優先順位
「入力値の検証をする」がよく対策として挙げられますが、これは多層防御の一枚目であって、本命ではありません。
| 優先度 | 対策 | 何をしているか |
|---|---|---|
| 1 | パラメータ化・プリペアドステートメント | 命令とデータを分離する |
| 2 | 出力先に応じたエスケープ | 解釈される文字を無害化する |
| 3 | 入力値の検証(許可リスト) | そもそも受け付けない |
1が本命で、2と3は補強です。1をやらずに3だけやっている状態が一番危ないと思います。
なお、XSSも2025年版ではInjectionに含まれます。
Reactが自動でエスケープしてくれるとはいえ、dangerouslySetInnerHTMLに外部由来の値を渡せば普通に発火します。
開発時のチェックポイント
- ユーザー入力をSQLに直接連結していないか?
- OSコマンドに直接渡していないか?
- ORMやプレースホルダを利用しているか?
- バインドできない箇所を許可リストで検証しているか?
- 出力先に応じたエスケープをしているか?
- 入力値の検証を行っているか?
6. Insecure Design(安全でない設計)
何が問題なのか?
Insecure Designは、実装以前の設計そのものにセキュリティ上の問題がある状態です。
例えば「ログインに5回失敗したらアカウントをロックする」という仕様があったとしても、第三者が意図的にログイン失敗を繰り返して他人のアカウントをロックできるのであれば、設計上の問題があります。
つまり、
コードが正しくても、仕様そのものが安全とは限らない
ということです。
開発時に意識すること
この例で言えば、仕様を変える必要があります。
| 仕様 | 攻撃者にできること | |
|---|---|---|
| ❌ | 5回失敗でアカウントロック | メールアドレスさえ知っていれば任意のアカウントを停止できる |
| ⭕️ | IP・デバイス単位のレート制限+段階的な遅延 | 総当たりの速度は落ちるが、他人を妨害できない |
実装は仕様どおりで、バグは1つもありません。それでも攻撃が成立します。
だからこのカテゴリは、コードレビューではなく設計レビューで潰すしかありません。
問うべきなのは「正しく動くか」ではなく、
「正しく実装されたとき、この機能は悪用できるか」
です。
開発時のチェックポイント
- この機能を悪用すると何ができるか?
- 攻撃者が想定外の使い方をしたらどうなるか?
- 権限の境界は明確か?
- レート制限は必要ないか?
- コストのかかる処理を無制限に呼べないか?
- セキュリティ要件を設計段階で定義しているか?
7. Authentication Failures(認証の失敗)
何が問題なのか?
Authentication Failuresは、ユーザー認証の仕組みに問題があることによって発生する問題です。
例えば、
- 弱いパスワードを許可する
- ログイン試行回数に制限がない
- セッション管理が不適切
- パスワードリセット機能が安全でない
などがあります。
開発時に意識すること
このカテゴリで残りやすいのが、ログアウトとパスワードリセットの後始末です。
| クライアント側 | サーバ側 | 盗まれていたトークンは | |
|---|---|---|---|
| ❌ | Cookieを削除 | 何もしない | ログアウト後も使える |
| ⭕️ | Cookieを削除 | セッションを破棄 | 使えなくなる |
// NG: クライアント側で消しているだけ
document.cookie = 'session=; Max-Age=0';
// OK: サーバ側で破棄してから消す
await sessionStore.destroy(sessionId);
「ログアウトできた」という見た目は同じなので、テストでは気づきにくい部分です。
パスワードリセットも同じで、「トークンを検証する」だけでなく、検証したら即座に無効化するまでがワンセットです。
開発時のチェックポイント
- ログイン試行回数を制限しているか?
- セッションを適切に管理しているか?
- パスワードリセット機能は安全か?
- リセットトークンは1回使ったら無効化されるか?
- MFAを検討しているか?
- ログアウト後にセッションが無効化されるか?
- パスワード変更時に他デバイスのセッションを切っているか?
8. Software or Data Integrity Failures(ソフトウェアやデータの整合性の問題)
何が問題なのか?
ソフトウェアやデータを無条件に信頼してしまうことによって発生する問題です。
A03のサプライチェーンと混同しやすいので、先に違いを整理しておきます。
| A03 Supply Chain Failures | A08 Integrity Failures | |
|---|---|---|
| 対象 | 上流のエコシステム | 信頼境界と完全性の検証 |
| 例 | パッケージの乗っ取り、パイプラインの侵害 | 署名検証なしの更新、安全でないデシリアライゼーション |
| 問い | この依存を信頼していいか? | このデータは改ざんされていないと言えるか? |
A03が「どこから来たものを使うか」の話なのに対して、A08は「手元にあるものを検証しているか」の話です。
開発時に意識すること
分かりやすい例がデシリアライゼーションです。
data = pickle.loads(request.body)
pickleはオブジェクトを復元する過程で任意のコードを実行できるため、外部から来たデータを渡すのはコード実行を許すのとほぼ同じです。
JWTも同様です。
// NG: 署名を検証していない
const payload = jwt.decode(token);
// OK: アルゴリズムを固定して検証する
const payload = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
jwt.decode()は署名を検証せずにペイロードを取り出すだけなので、これを認可判定に使ってはいけません。トークンの中のalgを信用して分岐する実装も危険です。
開発時のチェックポイント
- 外部から取得したデータを無条件に信頼していないか?
- 外部由来のデータをデシリアライズしていないか?
- JWTの署名を検証しているか?アルゴリズムを固定しているか?
- CI/CDの権限が過剰になっていないか?
- 重要な成果物の改ざんを検知できるか?
9. Security Logging & Alerting Failures(セキュリティログ・アラートの失敗)
何が問題なのか?
攻撃を受けても、
「何が起きたのか分からない」
という状態は非常に危険です。
2025年版で「Monitoring」から「Alerting」へ改名されました。
意図は明確で、ログを取っているだけで通知が飛ばないなら意味がない、ということです。
開発時に意識すること
何を記録すると、何に気づけるのか。対応で見るとこうなります。
| 記録するもの | 気づける攻撃 |
|---|---|
| 認証失敗(連続回数) | 総当たり、パスワードリスト攻撃 |
| 認可エラー(403の発生) | IDOR/BOLAの探索 |
| 権限変更・管理者操作 | 内部不正、乗っ取り後の権限昇格 |
| 1リクエストの返却件数 | 大量データの持ち出し |
| 404の大量発生 | ディレクトリ探索、.gitの探索 |
認可エラーの記録は特に効きます。正常に使っているユーザーは403をほとんど踏まないので、403が連続していれば、それは誰かがIDを書き換えて試している可能性が高いということです。
このカテゴリの特殊性
A09に紐づくCVEは723件しかありません。
これは問題が少ないからではなく、外部から検出できる種類の問題ではないので、CVEになりようがないからです。
つまりこれは、放っておいても誰も教えてくれないカテゴリです。
開発時のチェックポイント
- 認証失敗を記録しているか?
- 権限変更を記録しているか?
- 重要な操作を記録しているか?
- 認可エラー(403)の発生を記録しているか?
- 異常なアクセスを検知して通知できるか?
- ログにパスワードなどの機密情報を出していないか?
10. Mishandling of Exceptional Conditions(例外的な状況の不適切な処理)
何が問題なのか?
アプリケーションでは正常系だけではなく、タイムアウト、通信エラー、DB障害、権限エラー、不正な入力、予期しない例外など、様々な異常系が発生します。
2025年版で新しく追加されたカテゴリです。
ここで例外処理を誤ると、セキュリティ上の問題につながる可能性があります。
例えば、
try:
process()
except Exception as e:
return str(e)
のように例外内容をそのままユーザーへ返すと、内部情報が漏れる可能性があります。
開発時に意識すること
より危険なのは、例外が起きたときに処理が通ってしまうパターンです。
// NG: 例外が起きると allowed が true のまま残る
let allowed = true;
try {
allowed = await checkPermission(user, resource);
} catch (e) {
logger.warn(e);
}
if (allowed) {
// 権限チェックが失敗したのに通ってしまう
}
// OK: 拒否で初期化する
let allowed = false;
try {
allowed = await checkPermission(user, resource);
} catch (e) {
logger.error(e); // allowed は false のまま
}
考え方としては、異常時にどちらへ倒れるかです。
| Fail-Open(危険側) | Fail-Closed(安全側) | |
|---|---|---|
| 認可チェックが例外 | 通してしまう | 拒否する |
| 署名検証でエラー | 検証をスキップ | 拒否する |
| レート制限のRedisが落ちた | 無制限に通す | 拒否 or 保守的な制限 |
| WAFがタイムアウト | 素通し | ブロック |
可用性を優先してFail-Openにする判断もありえますが、認可・認証・署名検証だけは必ずFail-Closedにすべきだと思います。
セキュリティ判定の変数をtrueで初期化しない、というだけでもかなり違います。
開発時のチェックポイント
- エラー内容をユーザーへそのまま返していないか?
- 例外発生時に安全側へ倒れるか?
- セキュリティ判定の変数を「拒否」で初期化しているか?
- エラーによって認証・認可を迂回できないか?
- タイムアウト時の処理は適切か?
- ログには必要な情報が残っているか?
OWASP Top 10を「開発中の質問」に変えてみる
| カテゴリ | 開発中に自分へ問いかけること | |
|---|---|---|
| A01 | Broken Access Control | このユーザーは本当にこの操作をしていい? |
| A02 | Security Misconfiguration | デプロイ対象は「含めるもの」で決めている? |
| A03 | Software Supply Chain Failures | この依存ライブラリを信頼していい? |
| A04 | Cryptographic Failures | この値、クライアントに置いて大丈夫? |
| A05 | Injection | ユーザー入力を信用していない? |
| A06 | Insecure Design | そもそもこの仕様は安全? |
| A07 | Authentication Failures | ログアウトでサーバ側のセッションは消えている? |
| A08 | Software or Data Integrity Failures | このコードやデータは本当に信頼できる? |
| A09 | Security Logging & Alerting Failures | 攻撃された後に追跡できる? |
| A10 | Mishandling of Exceptional Conditions | エラーが起きたとき安全な状態になる? |
まとめ
OWASP Top 10は、セキュリティエンジニアだけが知っていればいいものではありません。
Webアプリケーションを開発するのであれば、開発者自身がある程度意識することで、脆弱性を「見つける」だけではなく、「作らない」ための考え方につながります。
10項目を並べてみて改めて思うのは、指摘してもらえるものと、そうでないものがあるということです。
A01やA05のように外から試せるものは、脆弱性診断やバグバウンティで見つかります。放っておいても、いずれ誰かが教えてくれます。
一方でA06(設計)、A09(ログ)、A10(例外処理)は、外から叩いても分かりません。開発している自分たちが気づかない限り、誰も指摘してくれないカテゴリです。
もちろん、OWASP Top 10を意識したからといって、すべての脆弱性を防げるわけではありません。
それでも、
「この実装、セキュリティ的に大丈夫かな?」
とコードを書く段階で一度立ち止まれるだけでも、大きな違いがあると思います。
もし今日1つだけ試すなら、自分が担当しているAPIを1本選んで、IDを他人のものに書き換えて叩いてみるのがおすすめです。
ローカルでテストアカウントを2つ作って、片方のトークンでもう片方のリソースを要求するだけです。200が返ってきたら、それがA01です。
セキュリティは、脆弱性診断をするときだけ考えるものではなく、設計・実装の段階から考えるもの。
これからも開発とセキュリティの両方の視点を持って、コードを書いていきたいと思います。