A06 安全性を欠いた設計

コードは書いた通りに動き、書いたことが間違っている。守るべき不変条件と信頼の境界を設計で決めていなければ、実装をどれだけ丁寧にしても直らない。

OWASP Top 10:2025 A06:2025 Insecure Design。対応づけられたCWEは39個。2021年版のA04から6位に下がった[1]。2026年11月時点の記載で書いています。

要点

  • 実装の不具合とは根の原因も直し方も違う。設計に無い守りは、コードの修正では足せない[1]。
  • 値段、合計、権限、持ち主、状態は、サーバーで導き直す。クライアントから受け取るのは利用者の意図だけ。
  • 複数の手順は、サーバーの状態機械として遷移ごとに確かめる。画面の順番は守りにならない。
  • 読んで、判断して、書く処理は競合状態になり得る。条件付きの更新か行ロックで1つの操作にする。
  • 直し方の入口は脅威モデリングと、誤用の場面を試験にすること。

成立の条件

A06は、守りの仕組みが設計に無いか、あっても効かない状態を指す。業務の流れの欠陥、つまり起きてはならない状態の変化を定めていないことも含む[1]。次の2つがそろうと通る。

  • 境界の誤り 利用者が書き換えられる場所にある値を、サーバーが判断に使う。
  • 不変条件の欠落 1枚のクーポンは1回だけ、在庫を超えて売らない、支払いの前に確定しない、のような規則が、コードのどこでも強制されていない。
クライアントからは利用者の意図だけを受け取り、値段や状態はサーバーで導き直す 利用者の端末。すべて書き換えられる 意図。受け取ってよい item_id, qty, coupon 形と範囲を確かめて使う 判断の材料。信用しない price, total, role, state 送られてきても捨てる 信頼の境界 サーバー 導き直す 値段と在庫は商品の表から、権限は セッションから、状態は注文の行から読む 不変条件を強制する 状態の遷移、回数、量の上限を、 DBの制約と条件付きの更新で守る
図1 信頼の境界と、サーバーで導き直す値

クライアントが決めた値を信用する

隠しフィールド、Cookie、JSONの本文、画面の非活性のボタンは、すべて利用者が書き換えられる。値段、小計、税、合計、割引をクライアントから受け取らない[2]。クライアントの側でだけ確かめる作りは CWE-602 に当たる。

悪い例と直した例。架空の通販の注文API

// 悪い例 送られてきた値段と合計をそのまま使う
const { itemId, qty, price, total } = req.body;
await charge(user, total);

// 直した例 受け取るのは商品と数量だけ。値段は商品の表から引き、合計はサーバーで計算する
const { itemId, qty } = req.body;
if (!Number.isInteger(qty) || qty < 1 || qty > MAX_QTY) throw new BadRequest();
const item = await db.one("SELECT price, stock FROM items WHERE id = $1", [itemId]);
if (qty > item.stock) throw new Conflict();
await charge(user, item.price * qty);

負の数量、0、極端に大きい数、小数も確かめる。数量が負なら合計が減る。業務の意味として正しい範囲かを確かめる検証で、形だけの検証とは別に要る[2]。

手順を飛ばす

画面が住所、支払い、確定の順に進んでも、APIは順番を知らない。確定のエンドポイントを直接呼ばれれば、支払いを経ずに注文が確定する。手順はサーバーの状態機械にし、遷移のたびに今の状態を確かめる[2]。CWE-841 に当たる。

画面の順番を飛ばして確定を直接呼ぶ道と、状態機械がそれを拒む所 cart address paid confirmed POST /orders/42/confirm を直接送る paid からの遷移だけ許す 状態を確かめない実装では通り、状態機械では cart からの遷移として拒まれる
図2 画面の順番を飛ばす要求と、サーバーの状態機械
// 直した例 許す遷移を表にし、更新の条件に今の状態を入れる
const NEXT = { cart: ["address"], address: ["paid"], paid: ["confirmed"] };
if (!NEXT[order.state]?.includes(to)) throw new Conflict();
const r = await db.query(
  "UPDATE orders SET state = $1 WHERE id = $2 AND state = $3", [to, order.id, order.state]);
if (r.rowCount !== 1) throw new Conflict();  // 間に別の要求が状態を変えた

競合状態

値を読み、判断し、書く処理は、どれも競合状態になり得る[2]。1回限りのクーポン、残高、在庫、招待の枠で、同時に送った要求が両方とも判断を通る。CWE-362 に当たる。

2つの要求が、どちらも未使用と読んでから使用済みに書くため、クーポンが2回使われる 要求A クーポンの行 要求B used = false SELECT used SELECT used false なので割引 false なので割引 UPDATE used UPDATE used 割引が2回通る
図3 読んでから書く間に、別の要求が割り込む
// 悪い例 読む、判断する、書くが別々の文
const c = await db.one("SELECT used FROM coupons WHERE code = $1", [code]);
if (c.used) throw new Conflict();
await applyDiscount(order, code);
await db.query("UPDATE coupons SET used = true WHERE code = $1", [code]);

// 直した例 判断を更新の条件に入れ、注文の更新と同じトランザクションにする
await db.tx(async (t) => {
  const r = await t.query(
    "UPDATE coupons SET used = true WHERE code = $1 AND used = false", [code]);
  if (r.rowCount !== 1) throw new Conflict();
  await applyDiscount(order, code, t);
});

条件付きの更新で書けないときは、SELECT ... FOR UPDATE の行ロックか、一意の制約で二重の登録を拒む。アプリの中のロックは、サーバーが複数台になった時点で効かなくなる。

回数と量の上限が無い

機能の正しい使い方を、機械で何千回も繰り返されることを設計で想定していない状態。OWASPは、15人までの団体予約を手付金なしで受ける映画館で、数回の要求で全館の数百席を押さえられる例を挙げる[1]。CWE-799 に当たる。

  • 利用者ごと、取引先ごと、送信元ごとの上限を、機能ごとに決める。紹介の特典、クーポン、新規登録の特典、予約の仮押さえは、特に悪用されやすい[2]。
  • 仮押さえには期限を付け、期限で自動に解く。
  • 発売の直後の数秒で買う、のような業務の上でありえない振る舞いを、検知と拒否の規則にする[1]。

守り方

根本の直し方
  • 機能を作る前に脅威モデリングをする。何を作るのか、何がまずくなり得るか、それにどう対処するか、十分だったか、の4つの問いで、認証、アクセス制御、業務の流れ、重要な手順を見る[1][4][5]。
  • 守るべき不変条件を要件として書く。ユーザーストーリーに、正しい使い方とあわせて誤用の場面を書き、受け入れの条件にする[1][6]。
  • 不変条件をコードの1か所で強制する。状態機械、条件付きの更新、DBの制約に置き、画面の制御に頼らない。
  • テナントの分離を、すべての層で設計の前提にする[1]。
  • 認証、権限の確認、ファイルの受け取りなどは、チームで確かめた共通の部品と、決まった作り方を使う[1][3]。

次は緩和の策で、根本の直し方を済ませたうえで重ねる。

  • 値を配る操作、たとえば返金、ポイントの付与、特典の発行をすべて記録し、量の急な変化で警告を出す[2]。
  • 不変条件、同時の実行、手順の順番を確かめる結合試験を置く。正しい使い方と誤用の両方の場面で書く[1]。

診断で見る所

  • 要求の本文の項目 値段、合計、権限、持ち主のID、状態のように、サーバーが自分で持っているはずの値が要求に入っていないか。入っていれば書き換えて送る。
  • 手順のエンドポイント 複数の手順の最後のAPIを、前の手順を経ずに呼ぶ。戻る、同じ手順を2回送る、別の注文の手順と混ぜる。
  • 同時の送信 1回限りの操作を、同じ要求を同時に送って試す。コードでは、SELECT の結果で判断してから UPDATE する並びを探す。
  • 境界の値 0、負の数、小数、上限を1つ超える値、非常に大きい数。
  • 試験の手引き OWASP WSTG の業務ロジックの試験の章が、データの検証、要求の偽造、整合性、処理の時間、機能の回数の制限、手順の回避の試し方を並べる[7]。

関連するCWE

A06には39個のCWEが対応づけられている[1]。このページの節に関わるものを挙げる。手順と競合状態の例は、MITREの各ページにもある[8][9]。

CWE名前このページの節
CWE-501Trust Boundary Violation成立の条件
CWE-602Client-Side Enforcement of Server-Side Securityクライアントが決めた値
CWE-472External Control of Assumed-Immutable Web Parameterクライアントが決めた値
CWE-642External Control of Critical State Dataクライアントが決めた値
CWE-841Improper Enforcement of Behavioral Workflow手順を飛ばす
CWE-362Concurrent Execution Using Shared Resource with Improper Synchronization競合状態
CWE-799Improper Control of Interaction Frequency回数と量の上限
CWE-653Insufficient Compartmentalizationテナントの分離
CWE-657Violation of Secure Design Principles全体

参照

  1. A06:2025 Insecure Design OWASP Top 10:2025
  2. Business Logic Security Cheat Sheet OWASP Cheat Sheet Series
  3. Secure Product Design Cheat Sheet OWASP Cheat Sheet Series
  4. Threat Modeling Cheat Sheet OWASP Cheat Sheet Series
  5. Threat Modeling Manifesto Threat Modeling Manifesto Working Group
  6. Abuse Case Cheat Sheet OWASP Cheat Sheet Series
  7. Business Logic Testing OWASP Web Security Testing Guide
  8. CWE-841 MITRE CWE
  9. CWE-362 MITRE CWE