A10 例外的な状況への不適切な対応
正常な道筋は試験され、例外の道筋は試験されない。攻撃者は例外を起こしてその道筋へ入り、開いたままの扉、漏れる内部の情報、半分だけ進んだ状態を使う。
要点
- 失敗は、防げない、起きている間に気づけない、起きた後の対応がまずい、の3つの形で現れる[1]。
- 守りの判断で例外が起きたら拒否に倒す。例外を握りつぶして処理を続ける作りは、失敗した時に開く。
- 利用者には中身の無い誤りの応答と照合用のIDだけを返し、詳細はサーバーのログに書く。
- 複数の手順は1つのトランザクションにし、途中で失敗したら全体を戻す。
- 例外は起きた所で捕まえて意味のある対応をし、全体の受け止め役は最後の網として置く。
成立の条件
OWASPは原因に、入力の検証の欠落、誤りの処理が呼び出しの深い所でなく高い所に置かれていること、メモリ、権限、ネットワークの想定外の状態、例外の扱いのばらつき、捕まえていない例外を挙げる。結果は、論理の誤り、競合状態、不正な取引、認証と認可の問題として、機密性、可用性、完全性のどれをも損ない得る[1]。
| 形 | 典型 | このページの節 |
|---|---|---|
| 防げない | 欠けた引数、余計な引数、0での割り算、null の参照を、入口で弾かない | 診断で見る所 |
| 気づけない | 例外を捕まえて何もしない。戻り値を確かめない | 失敗した時に開く |
| 対応がまずい | 拒否でなく許可に倒れる。詳細を返す。途中の状態を残す。資源を放さない | 以下の4つの節 |
失敗した時に開く
権限の確認、署名の検証、外部の認可サービスへの問い合わせで例外や時間切れが起きた時、例外を握りつぶして処理を続ければ、確認が無かったことと同じになる。CWE-636 に当たる[6]。
悪い例と直した例。架空の文書APIの権限の確認
// 悪い例 認可サービスが落ちたら、確認を飛ばして通す
let allowed = true;
try { allowed = await authz.check(user, "doc:read", docId); }
catch (e) { logger.warn(e); } // allowed は true のまま
if (!allowed) return res.sendStatus(403);
// 直した例 既定は拒否。許可の結果を受け取れた時だけ通す
let allowed = false;
try { allowed = (await authz.check(user, "doc:read", docId)) === true; }
catch (e) { logger.error({ event: "authz_error", err: e }); return res.sendStatus(503); }
if (!allowed) return res.sendStatus(403);
- 判断の変数の初期値を拒否にし、明示の許可でだけ通す。
=== trueで比べ、undefinedや誤りのオブジェクトを許可と読まない。 - 何でも受ける
catchで例外を捕まえ、記録だけして続ける形は、CWE-396 と CWE-390 に当たる。レビューではまずここを探す。
詳しすぎるエラー
スタックトレース、SQLの誤り、ファイルの置き場所、部品の版を利用者に返すと、攻撃の下調べを助ける。OWASPの例では、詳しいDBの誤りを表示させて系の情報を集め、SQLインジェクションの組み立てに使う[1][2]。CWE-209 に当たる[7]。
悪い例と直した例。架空のAPIの応答
# 悪い例 誤りをそのまま返す
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain
error: column "emial" does not exist
at Parser.parseErrorMessage (/srv/shop/node_modules/pg-protocol/dist/parser.js:283)
# 直した例 中身の無い応答と照合用のIDだけを返す
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
{"type":"about:blank","title":"Internal Server Error","status":500,
"errorId":"7f3c2a9e"}
RFC 9457 は、HTTP APIの誤りの応答の形式を定める。type を about:blank にした時、title はHTTPの状態の文言と同じにすべきとされ、独自の項目を足せる[3]。OWASPは、誤りの詳細はサーバーの側で記録し、利用者には一般的な応答を返し、実行環境の設定として全体の誤りの受け止め役を置くよう勧める[2]。
Express の全体の受け止め役。引数が4つの関数を、ほかの経路の後に置く[8]
app.use((err, req, res, next) => {
const errorId = crypto.randomUUID();
logger.error({ event: "unhandled_error", errorId, err, path: req.path });
res.status(500).type("application/problem+json").json(
{ type: "about:blank", title: "Internal Server Error", status: 500, errorId });
});
本番の設定で、フレームワークの詳細の誤り画面とデバッグの出力を止める。CWE-215 と CWE-756 に当たる。利用者が原因の誤りは 4xx で返し、5xx は予期しない失敗にだけ使う。5xx の増加は、特定の入力で落ちている兆しとして監視する[2]。
途中で止まった複数の手順
引き落とし、入金、記録のような複数の手順の途中で失敗した時、済んだ手順を戻さなければ、状態が壊れる。OWASPの例では、攻撃者が通信を切って送金の途中で止め、全体が戻されない作りで口座を空にするか、競合状態で何度も送金する[1]。
// 悪い例 手順ごとに確定する。入金で例外が出ると、引き落としだけが残る
await db.query("UPDATE accounts SET balance = balance - $1 WHERE id = $2", [amt, from]);
await db.query("UPDATE accounts SET balance = balance + $1 WHERE id = $2", [amt, to]);
await db.query("INSERT INTO transfers (req_id, from_id, to_id, amount) VALUES ($1, $2, $3, $4)", [reqId, from, to, amt]);
// 直した例 1つのトランザクションにし、残高の条件と、要求ごとの一意の制約で二重の実行を拒む
await db.tx(async (t) => {
await t.query("INSERT INTO transfers (req_id, from_id, to_id, amount) VALUES ($1, $2, $3, $4)", [reqId, from, to, amt]);
const r = await t.query(
"UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1", [amt, from]);
if (r.rowCount !== 1) throw new Conflict();
await t.query("UPDATE accounts SET balance = balance + $1 WHERE id = $2", [amt, to]);
}); // どこで例外が出ても全体が戻る
OWASPは、複数の手順の操作が失敗したら、部分的に立て直そうとせず、トランザクション全体を戻すよう求める[1]。DBの外の副作用、たとえば外部の決済やメールの送信は戻せないので、確定の後に送るか、要求ごとのIDで何度送っても1回分になるようにする。
例外の時に放さない資源
OWASPの例では、ファイルの受け取りで例外を捕まえても資源を放さず、失敗のたびに資源が固定され、尽きるまで続く[1]。攻撃者は失敗する要求を送り続けるだけで、サービスを止められる。CWE-460 に当たる。
// 悪い例 例外が出ると close と release に届かない
const lock = await locks.acquire(userId);
const fh = await fs.open(tmpPath, "w");
await receiveUpload(req, fh); // 途中で切断されると例外
await fh.close();
await lock.release();
// 直した例 取った順の逆に、finally で必ず放す
const lock = await locks.acquire(userId);
try {
const fh = await fs.open(tmpPath, "w");
try { await receiveUpload(req, fh); }
finally { await fh.close(); }
} finally {
await lock.release();
}
ファイルの記述子、DBの接続、ロック、一時ファイル、メモリの上限を、失敗の道筋でも放す。言語の仕組み、たとえば Java の try-with-resources や C# の using を使えば書き忘れが減る。あわせて、要求の大きさ、時間、同時の数に上限を置き、例外的な状況そのものを起こしにくくする[1]。
守り方
次は緩和の策で、根本の直し方を済ませたうえで重ねる。
- 回数の制限、資源の割り当て、絞り込みで、例外的な状況とサービスの妨害の危険を下げる[1]。
- 同じ誤りが高い率で繰り返す時は、件数と期間をまとめた記録にする。記録と監視は A09 と組で設計する[1]。
- 負荷の試験、性能の試験、侵入の試験で、例外の道筋を通す[1]。
診断で見る所
- 例外を起こす入力 必須の引数を外す、余計な引数を足す、型を変える、0、空、巨大な値、壊れたJSON、途中で切れた本文。応答に内部の情報が出ないか、状態が残らないか。
- 依存先の失敗 認可サービス、DB、外部APIが時間切れになった時に、許可に倒れないか。試験の環境で依存先を止めて確かめる。
- コードの形 何でも受ける
catchで記録だけして続ける所、戻り値を確かめない所、switchのdefaultの欠落、finallyの無い資源の取得。 - 本番の設定 デバッグの出力、詳細の誤り画面、独自の誤りのページの有無。
- 試験の手引き OWASP WSTG の誤りの扱いの試験[5]。OWASP ASVS 5.0 の V16 に、誤りの扱いの検証の要件がある[4]。
関連するCWE
A10には24個のCWEが対応づけられている[1]。このページの節に関わるものを挙げる。
| CWE | 名前 | このページの節 |
|---|---|---|
| CWE-703 | Improper Check or Handling of Exceptional Conditions | 全体の親 |
| CWE-636 | Not Failing Securely | 失敗した時に開く |
| CWE-390 | Detection of Error Condition Without Action | 失敗した時に開く |
| CWE-396 | Declaration of Catch for Generic Exception | 失敗した時に開く |
| CWE-209 | Generation of Error Message Containing Sensitive Information | 詳しすぎるエラー |
| CWE-215 | Insertion of Sensitive Information Into Debugging Code | 詳しすぎるエラー |
| CWE-756 | Missing Custom Error Page | 詳しすぎるエラー |
| CWE-460 | Improper Cleanup on Thrown Exception | 途中で止まった手順、放さない資源 |
| CWE-252 | Unchecked Return Value | 診断で見る所 |
| CWE-234 | Failure to Handle Missing Parameter | 診断で見る所 |
| CWE-478 | Missing Default Case in Multiple Condition Expression | 診断で見る所 |
参照
- A10:2025 Mishandling of Exceptional Conditions OWASP Top 10:2025
- Error Handling Cheat Sheet OWASP Cheat Sheet Series
- RFC 9457 Problem Details for HTTP APIs IETF。2023年7月、RFC 7807 を置き換え。4.2.1
- V16 Security Logging and Error Handling OWASP ASVS 5.0.0
- Testing for Improper Error Handling OWASP Web Security Testing Guide
- CWE-636 MITRE CWE
- CWE-209 MITRE CWE
- Error Handling Express