business-logic-testing について
business-logic-testingは、アプリケーションの不変条件、状態遷移、数値処理、認可、並行処理を許可された範囲でテストするスキルです。ステージング環境や管理下のアカウント、合意済みのリクエスト上限を使い、永続状態の変化を証拠として確認します。
主な機能
- 01ビジネス不変条件のモデル化
- 02アクター・操作・リソースの権限テスト
- 03状態遷移の悪用を検証
- 04数値と価値の境界条件を確認
- 05管理されたリクエスト数で競合をテスト
活用シーン
- 01クーポンや注文を二重に利用できないか確認
- 02別テナントのレコードを操作できないかテスト
- 03並行リクエストで残高が超過しないか検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ briiirussell/ cybersecurity-skills --skill business-logic-testingインストール後、Claude Code で/business-logic-testingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/concurrency.mdfindings-format.mdfindings.schema.jsonseverity-calibration.mdto-sarif.mjsvalidate-findings.mjsverification.md詳細
ビジネスロジックのテスト — 不変条件、ステートマシンの悪用、競合状態
スキャナーでは見つけられない脆弱性の種類です。スキャナーは構文を照合します。一方、ロジックの欠陥とは、アプリが保証するはずのことと、敵対的な操作や同時利用の下でコードが実際に強制することとの不一致です。このスキルは、owasp-audit のソースコード側の競合パターン(TOCTOU、アトミックなキュー取得、予約してから実行する処理)に対応する動的テストであり、web-pentest のフェーズ 8 をさらに深く掘り下げます。
まず不変条件を定めます。保証内容を明示してから、それを破ろうとしてください。発見事項は「妙なリクエストを送った」ではありません。「明示された不変条件に違反し、永続化される状態が変化したことを証明した」です。
認可チェック
並行性テストやロジックテストでは、実際の状態が変更されます。何かを送信する前に、以下を確認してください。
- 対象のアプリケーションを特定し、ビジネスロジックと並行性のテストが対象範囲に含まれることを明記した書面による許可。
- ステージング環境またはテストモードの環境を優先してください。 決済テストモード(例:Stripe のテストキー)、テストデータを投入したテナント、自分が所有する使い捨てアカウントを使用してください。書面による許可の対象範囲に明記されていない限り、本番の資金フロー、在庫台帳、またはサードパーティの請求 API を試さないでください。
- 事前に合意したリクエスト数の上限。並行性を証明するには数十件の並列リクエストで十分であり、数千件は不要です。上限を定め、それを超えないようにしてください。
- テストするすべてのロールとテナントについて、自分が管理するアカウント(オーナー、非オーナー、管理者、2 つのテナント)。
いずれかが欠けている場合は中止して、確認を求めてください。以下の境界を参照してください。
実際のリクエストの送信は、このスキルで事前承認されているツール(Read, Grep, Glob のみ)の対象外です。並列リクエストは人間が実行するか、ツールを呼び出すたびに承認を得てください。こうすることで、スループットテストが無人で実行されることを防ぎます。
ステップ 1 — 不変条件をモデル化する
リクエストに触れる前に、システムが何を保証するのかを書き出してください。金銭やワークフローに関するバグの大半は、次の 4 種類でカバーできます。
- 価値の保存。 金銭、クレジット、ポイント、在庫、クォータは、認可された経路以外では増減しません。10 ドルの返金では、クレジットが正確に 10 ドル分減る必要があります。クーポンを利用すると、残りの利用回数が減る必要があります。
- 一意性。 クーポンはルールごとに 1 回だけ利用でき、ユーザー名、招待、冪等性キーはちょうど 1 つの主体に割り当てられ、注文は 1 回だけ処理されます。
- 単調性。 一部のカウンターは一方向にしか変化しません。使用済みのバウチャーが未使用に戻らない、出荷済みの注文が保留中に戻らない、消費済みのワンタイムトークンが再び有効にならない、といった状態です。
- 排他性。 座席、ロック、リース、「承認待ち」の行は、一度に 1 人のアクターだけが保持します。同じ相互排他的なリソースを 2 者が同時に所有することはできません。
各不変条件について、どこで強制されているか(DB の一意制約か、アプリケーションの if 文か、UI だけか)と、違反を何で証明できるか(ゼロにならなくなった台帳、存在するはずのない 2 件の行など)を記録してください。UI だけ、または単一のハンドラーだけで強制されている不変条件が、重点的に調べるべき箇所です。
ステップ 2 — アクター × アクション × リソースのマトリックス
組み合わせをすべて列挙し、アプリが考慮し忘れたセルを探してください。
- アクター: 匿名ユーザー、認証済みユーザー、リソースのオーナー、非オーナー、管理者、別々のテナント 2 つ、ワークフローの途中にいるユーザー。
- アクション: 作成、読み取り、更新、削除、承認、キャンセル、返金、利用、譲渡、状態の遷移。
- リソース: ライフサイクルまたは残高を持つ各オブジェクト — 注文、請求書、クーポン、サブスクリプション、ドキュメント、承認リクエスト、ウォレット。
各セルに期待される結果(許可/拒否)を記入し、拒否すべきセルと、状態に応じて許可されるセルをテストしてください。注目すべき失敗は、非オーナーがオーナー向けの操作を実行できること(IDOR/BOLA と重複します。web-pentest のフェーズ 4 を参照)、別の状態でのみ有効なアクションをアクターが実行できること、または同じアクターが一度限りのアクションを 2 回実行できることです。
ステップ 3 — 状態機械の悪用
想定される状態グラフを描きます(カート -> チェックアウト -> 支払い済み -> 発送済み、下書き -> 送信済み -> 承認済み)。次に、各遷移を攻撃します。
- ステップを飛ばす。 ステップ 2 を完了せずに、ステップ 4 のエンドポイントを直接呼び出します。請求なしで
paidに到達できますか? または、レビュー担当者なしでapprovedに到達できますか? - ステップを並べ替える。 合計金額が確定した後に割引を適用します。支払い承認後、売上確定前に配送先住所を変更します。
- ステップを再実行する。 終端状態への遷移を再度 POST します。「確認」が 2 回実行されますか? 使い捨てトークンが再び有効になりますか?
- 上限の分割回避。 上限がリクエストごとに確認され、累計では確認されない場合、上限超過の操作を上限未満の複数の操作に分割します(1 日の上限額を複数の少額送金で引き出す、累計を共有しない複数のアップロードでアップロードごとの上限を超える)。
- 自分の申請を自分で承認する。
approver_id/reviewer_idを改ざんし、申請者と承認者を同じプリンシパルにします。
これらはいずれも、ステップ 6 の並行実行による増幅の候補です。順次実行では狙いにくい時間枠も、並列リクエストなら簡単に突けます。
ステップ 4 — 数値と値の問題
- 浮動小数点数と十進数。 浮動小数点数として保存または計算された価格や残高には、丸め誤差が蓄積します。
0.1 + 0.2のような金額、小数部の長い数量、丸め境界付近の値を試し、1 セントが現れたり消えたりしないか確認します。 - 丸める方向。 明細ごとの丸めが常に顧客に有利な方向に行われ、多数の明細にわたって合算されると、価値が漏れます。少額の明細を多数購入すると、正しい合計額より安くなることがあります。
- 負数とゼロの金額。 数量
-1で注文できますか? 負の金額を送金して自分の口座に入金できますか? 小計を超える割引を適用して合計をマイナスにできますか? - 通貨換算を利用した返金の裁定取引。 ある通貨で支払い、そのときの為替レートで換算し、別の通貨で(または後のレートで)返金して、差額を得ます。請求と返金で異なる為替レートのスナップショットや異なる通貨が使われる場合をテストします。
- 型/精度の強制変換。 指数表記(
1e3)、過剰な精度、先頭のゼロ、または金額型の境界での整数オーバーフローを確認します。
ステップ 5 — 古い認可情報とサーガの隙間
- 古い機能ゲートとロール。 ダウングレード、キャンセル、またはロール変更の後、キャッシュの有効期限が切れるまで古い権限が使えませんか? 権限を取り消した直後にゲートで制限された操作を試し、変更前に発行されたトークンでも試します。
- 新たな意思確認なしで実行できる機密性の高い操作。 出金、支払い、メールアドレスやパスワードの変更、MFA の変更、API キーの作成には、ログイン済みページで動作するスクリプトでは完了できない追加認証(再認証、パスキー、またはページ外での確認)が必要です。既存のセッションだけで、それぞれの操作を試します。新しい認証情報を使わずに、ブラウザーコンソールまたはプロキシからリクエストを再送します。成功する場合、そのページ上のサードパーティスクリプトも同じ操作を実行できます(
breach-patternsパターン 11)。この問題の深刻度は、その操作で動かせるものによって決まります。 - イベント駆動型サーガの隙間。 イベントで調整される複数サービスのワークフロー(注文 -> 支払い -> 在庫 -> フルフィルメント)には、単一のトランザクションがありません。次の点を確認します。先行するステップの補償処理が実行される前にコミットするステップ、部分的な失敗時に実行されない補償処理(返金、在庫の解放)、再配信されたイベントを二重適用する冪等性のないコンシューマーです。動的な兆候は、中断または重複したフローの後に、サービス間で内部状態の整合性が取れなくなることです。
ステップ 6 — 並行処理の手法
ほとんどのロジック上の不変条件は、逐次実行なら安全でも、並列実行では安全でないチェック・アンド・アクトの処理で守られています。目標は、同じチェック・アンド・アクトのウィンドウ内に N 件のリクエストを着地させ、どのアクトもコミットされる前にチェックを N 回通過させることです。
調査すべきパターン(ソース側の同等項目は owasp-audit A06/A02/A08 にあります):
- プリンシパルではなくパスに紐付けられた冪等性キー。 キーが
(principal, endpoint, key)ではなく(endpoint, key)に紐付けられている場合、別のユーザーのキー、または自分のキーのリプレイによって、完了済みの結果が再利用されます。1 つのキーで異なる 2 つの操作を送信します。1 つのキーを 2 つのアカウントで使用します。 - コミット前のキャッシュ更新。 アプリがキャッシュに「予約済み/保留中」のマーカーを書き込んでから DB にコミットします。2 件のリクエストが、どちらのコミットも反映される前に、古いキャッシュから「利用可能」を読み取ります。
- 更新の喪失(読み取り・変更・書き込み)。
balance = read(); write(balance - amount)がアトミックなデクリメントやバージョンチェックなしで実行される場合、並行する各書き込み処理は同じ読み取り結果を基に開始し、最後の書き込みが勝って他の処理の効果が失われます。 - Saga の補償タイミング。 順方向のステップとその補償処理の間のウィンドウで、並列バーストを発生させます。
NX/EXまたはフェンシングトークンのない Redis ロック。 アトミックな取得と有効期限設定を伴わないロック、またはフェンシングトークンのないロックでは、2 つの取得者がどちらもロックを所有していると信じてしまいます(片方が取得した後にもう片方が上書きする、または GC による一時停止から復帰した取得者がリースの期限切れ後に処理を実行する)。- READ COMMITTED の異常。 デフォルトの分離レベルでは、反復不能読み取りや書き込みスキューが許容されます。2 つのトランザクションがそれぞれ制約を満たしていると読み取り、両方が書き込むことで制約違反が発生します。一意性を(DB の UNIQUE 制約ではなく)
SELECT ... then INSERTで強制している場合、ここで破綻します。 - Upsert 競合。 「存在しなければ挿入、存在すれば更新」をアプリコードで実装している場合(アトミックな
INSERT ... ON CONFLICTではない場合)、並行実行によって二重挿入が発生します。
同じウィンドウへのリクエストの着地。 Web リクエストにおけるスターターピストルに相当する手法:
- HTTP/2 のシングルパケット攻撃。 多数のリクエストの最終フレームを 1 つの TCP パケットにまとめ、サーバーに同時に受信させることで、競合時のネットワークジッターを排除します。
- 最終バイトの同期(HTTP/1.1)。 Keep-Alive 接続を通じて各リクエストの最終バイト以外をすべて先に送信し、最後のバイトをまとめて送信します。
- 接続のウォームアップ。 先に使い捨てのリクエストを送信し、計測対象のバーストの前に TLS/接続のセットアップと遅延初期化を済ませます。
ツール、実例付きの Turbo Intruder 設定、および最小限の httpx/curl --parallel ランナーは、references/concurrency.md にあります。いずれも、許可されたテスト用途と合意済みのリクエスト上限の範囲内で使用します。
検証と誤検知 — クラス別
候補を指摘事項として扱う前に、すべての候補が証拠の基準を満たしているか確認してください。判定用語は、候補を confirmed、needs_validation、または rejected とします(references/verification.md を参照)。重大度を付与するのは confirmed の指摘事項のみです。references/severity-calibration.md を参照して調整してください。
一般的な検証基準(全クラス対象): 1 件のリクエストが正しく拒否された後、本来なら 1 件だけ成功するはずのところで、N 件の並行リクエストが成功し、永続化された状態の変化が証明されている(台帳の合計が合わなくなった、一意であるべきなのに 2 行存在する、単調増加の上限を超えたカウンター、単一の認可済みアクションでは許容されないほど変動した残高など)。レスポンスコードが変わっただけでは証拠になりません。永続化された状態を読み戻してください。
| クラス | 確認済みとする条件 | 誤検知の可能性が高いケース |
|---|---|---|
| 二重使用/クーポンの再利用 | N 回まで使用できるクーポンが N 回を超えて使用された、返金額が請求額を上回った、在庫が過剰販売されたことを残高や件数の読み戻しで証明できる | 処理が実際に冪等である — ETag/If-Match による強制、または DB UNIQUE/ON CONFLICT によって重複がまとめられ、1 件だけがコミットされる |
| 更新の消失 | 並行処理による差分の合計が、永続化された残高の純変動と一致しない | アトミックな減算、またはバージョン/WHERE balance >= amount ガードにより、1 件を除くすべてが拒否された |
| 冪等性キーの再利用 | 2 回目の別の操作(または別のプリンシパル)がキーを再利用し、最初の結果を受け取った、またはリプレイによって二重に適用された | キーがプリンシパルとリクエスト本文のハッシュに正しくスコープ設定されており、副作用を再適用せずにリプレイへキャッシュ済みの結果を返す |
| 状態機械のスキップ/順序変更/リプレイ | 前提条件を満たさずに、終端状態または禁止された状態へ永続的に遷移した | 遷移はサーバー側で拒否され、UI だけが進んだ |
| 制限の分割回避 | 分割したリクエスト全体で集計すると、制限を超過した状態が永続化されている | サーバー側の累積合計によって全体の超過が検出された |
| 数値(負数/丸め/裁定取引) | 最終的に永続化された残高に、生成または消失した価値が反映されている | 入力検証で拒否された。または「差異」は表示形式だけの問題だった |
| 古いゲート/サーガの欠落 | 取り消された権限で永続的な操作が実行された、またはフローの中断/重複後にサービス間で不整合が残った | 有効期間が短く、文書化されたキャッシュ TTL が、永続的な影響を残さず自動的に解消する |
必ず除外する 2 つの誤検知パターン:
- 真に冪等な操作。 ETag/
If-Matchによる強制、DB UNIQUE 制約、またはINSERT ... ON CONFLICT DO NOTHINGにより、重複は 1 件のコミットにまとめられます。並行リクエストは受け入れられたように見えても(すべて 200)、実際に反映されたのは 1 件だけです。必ず永続化された状態を読み戻し、ステータスコードを信用しないでください。 - 見た目だけの不具合。 トーストが二重に表示されること、2 件のアイテムを表示する楽観的 UI、再取得前の一時的な件数は、サーバーの状態が正しければ指摘事項ではありません。永続的な状態変化がなければ指摘事項ではなく、記録するとしてもハードニングに関する注記にとどめ、重大度は付与しないでください。
合意したリクエスト上限内で成立させられなかった候補は、確認済みの指摘事項ではなく、validation_plan(ステージング環境で実行する正確なバースト)を添えた needs_validation とします。
出力形式
共通の指摘事項用語に従って報告してください。findings.json のレコードと検証/エクスポート方法については、references/findings-format.md を参照してください。
# Business Logic & Race Condition Test Report
## Target / environment: [app + staging vs prod, test-mode confirmation]
## Authorization: [scope reference, request-count cap]
### Invariant register
| Invariant | Family | Enforced where | Violation proof |
### Findings
#### [verdict] [Title]
**Invariant violated:** [conservation / uniqueness / monotonicity / exclusivity]
**Endpoint(s):** `METHOD /path`
**Preconditions:** [accounts, state, concurrency needed]
**Reproduction:** [the controlled burst — count, sync method; or the step sequence]
**Durable-state proof:** [the balance/row/counter read back that shows the violation]
**Verdict:** confirmed | needs_validation (+ blockers, validation_plan) | rejected (+ ruled_out_by {control, location})
**Severity:** [only if confirmed — see severity-calibration.md]
### Hardening notes (not findings)
[Visual-only glitches, defense-in-depth gaps with no traced durable impact]
境界
- 明示的な書面によるスコープで対象として指定されていない限り、本番環境の資金フロー、在庫台帳、または稼働中のサードパーティ決済 APIに対する並行処理テストやロジックテストの実行は拒否してください。デフォルトではステージング/テストモードを使用してください。
- 合意したリクエスト数の上限を超えたり、概念実証を継続的な負荷に変えたりすることは拒否してください。制御された二重支払いの実証に必要なのは十数件のリクエストであり、ストレステストではありません(それは DoS です。
web-pentestの境界を参照してください)。 - サードパーティのシステム(決済プロバイダー自身の API、上流の SaaS)は、スコープ内のアプリが呼び出している場合でも、デフォルトではスコープ外のため、決して対象として触れてはいけません。
- 概念実証は違反を実証するために必要な最小限のバーストであり、繰り返し抽出に使える再利用可能なエクスプロイトではありません。
- テストで実際の資金や在庫が動いた場合は、停止して報告してください。状態をさらに変更する補償取引を発行して「後始末」してはいけません。
相互参照
owasp-auditA06/A02/A08 — ソース側の競合パターン:TOCTOU、アトミックなキューの取得(UPDATE ... WHERE status='pending' RETURNING)、予約してから実行する処理、外部リソース作成時の TOCTOU。このスキルで動的に実証するチェック後・実行前の隙間を見つけるため、コードを読んでください。web-pentestフェーズ 8(ビジネスロジック)と「ホワイトボックス+ブラックボックスの複合モード」— ソースコードと実行中のインスタンスの両方を利用できる場合、コード上の指摘から競合させるべき正確な隙間を特定できます。api-audit— 冪等性、BOLA、API レベルのオブジェクト/機能の認可。finding-triage— 各候補を、状況を踏まえた重大度で処置方針が決まるまで評価してください。
参考資料
- Strix(Apache-2.0)—
vulnerabilities/race_conditions.mdとvulnerabilities/business_logic.md(https://github.com/usestrix/strix)。手法は言い換えており、コピーしていません。このスキルでは、Strix の「決して拒否しない/認可に疑問を持たない」という姿勢は意図的に採用していません。認可、ステージングの優先、リクエスト数の上限をここでは徹底します。 - PortSwigger Web Security Academy — 競合状態(単一パケット攻撃)およびビジネスロジックのラボ。
- OWASP Web Security Testing Guide(WSTG)— ビジネスロジックテストのセクション。