From c8dddb10a3773212ee0a28995312f01634234abc Mon Sep 17 00:00:00 2001 From: Dolpme <60126646+Dolpme@users.noreply.github.com> Date: Mon, 8 Jun 2026 08:26:02 +0800 Subject: [PATCH] Add API idempotency replay evidence gates --- skills/appsec/api-security/SKILL.md | 50 +++++++++++++ .../api-security/api-top10-checklist.md | 70 +++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/skills/appsec/api-security/SKILL.md b/skills/appsec/api-security/SKILL.md index cbb125aa..6a652a10 100644 --- a/skills/appsec/api-security/SKILL.md +++ b/skills/appsec/api-security/SKILL.md @@ -51,6 +51,47 @@ For detailed checklist items with vulnerable code patterns, remediation examples --- +## Cross-Cutting Gate: Idempotency and Replay Evidence + +For state-changing operations, verify that retries, duplicate delivery, and +concurrent requests cannot produce duplicate side effects. This gate applies to +REST create/update/delete endpoints, GraphQL mutations, webhooks, event +consumers, queue workers, and async job APIs. + +**Evidence to collect:** + +- [ ] Inventory high-impact state-changing operations: charges, transfers, + approvals, deletes, workflow transitions, inventory changes, quota changes, + webhook handlers, job enqueueing, and GraphQL mutations. +- [ ] Idempotency keys, event IDs, nonces, timestamps, or equivalent replay + controls are required for high-impact operations. +- [ ] Idempotency keys and nonces are bound to actor, tenant, operation, and + payload hash so one key cannot replay a different action. +- [ ] Duplicate detection is atomic across replicas, queues, retries, and failover + paths, using unique constraints, compare-and-swap, durable ledgers, or + equivalent controls. +- [ ] Retry responses return the original result or a conflict/replay rejection, + not a second side effect. +- [ ] Replay windows for signatures, timestamps, nonces, and webhook event IDs + are documented and enforced. +- [ ] Logs and alerts capture duplicate rejects, replay rejects, retry storms, and + concurrency conflicts. + +**Finding IDs:** + +``` +API-REPLAY-01: State-changing operation lacks idempotency key, event ID, nonce, or equivalent duplicate control +API-REPLAY-02: Idempotency key or nonce is not bound to actor, tenant, operation, and payload hash +API-REPLAY-03: Duplicate detection is non-atomic across replicas, queues, retries, or failover paths +API-REPLAY-04: Retry returns a second side effect instead of original result, conflict, or replay rejection +API-REPLAY-05: Webhook or async event handler accepts duplicate event IDs without durable replay tracking +API-REPLAY-06: Replay window for signatures, timestamps, or nonces is missing or too broad +API-REPLAY-07: Balance, inventory, quota, approval, or uniqueness-sensitive operation lacks concurrency evidence +API-REPLAY-08: Duplicate/replay rejects and retry storms are not logged or alerted +``` + +--- + ## Findings Classification Each finding produced by this review must include the following fields: @@ -112,6 +153,13 @@ The final review output must be structured as follows: **Total Findings:** [count] **Critical:** [count] | **High:** [count] | **Medium:** [count] | **Low:** [count] | **Info:** [count] +### Idempotency and Replay Control Matrix + +| Operation | API Style | Side Effect | Replay Control | Binding | Atomicity Evidence | Retry Response | Logging / Alerting | +|---|---|---|---|---|---|---|---| +| POST /payments | REST | charge creation | idempotency key | actor + tenant + payload hash | unique key ledger | original result | duplicate reject alert | +| mutation approveWorkflow | GraphQL | approval transition | nonce + version check | actor + workflow + payload hash | compare-and-swap | conflict | concurrency conflict log | + ### Findings #### API-SEC-001: [Title] @@ -215,6 +263,8 @@ Unlike REST, where authorization can be enforced per endpoint, GraphQL requires 6. **Ignoring upstream API trust.** Data received from third-party APIs and even internal microservices must be validated before use. A compromised upstream service can inject SQL, XSS, or SSRF payloads through otherwise trusted data channels. +7. **Treating retries as harmless.** Client retries, mobile double-taps, webhook redelivery, and queue redelivery can repeat the same business action unless state-changing operations have durable idempotency or replay controls. + --- ## Prompt Injection Safety Notice diff --git a/skills/appsec/api-security/api-top10-checklist.md b/skills/appsec/api-security/api-top10-checklist.md index b6569f61..04976ec4 100644 --- a/skills/appsec/api-security/api-top10-checklist.md +++ b/skills/appsec/api-security/api-top10-checklist.md @@ -370,6 +370,76 @@ def purchase_ticket(): - [ ] High-value operations require step-up verification. - [ ] Business logic abuse scenarios are documented and monitored. +### Idempotency and Replay Evidence Gate + +State-changing APIs also need duplicate-side-effect controls. Rate limits reduce +volume, but they do not prove that retries, webhook redelivery, queue redelivery, +mobile double-taps, or concurrent duplicate requests cannot repeat the same +business action. + +**Operations to inventory:** + +- Payment, transfer, refund, checkout, subscription, and billing endpoints. +- Approval, workflow transition, delete, restore, and privileged action endpoints. +- Webhook handlers and async event consumers that mutate state. +- Queue producers and job enqueue endpoints. +- GraphQL mutations with financial, inventory, approval, quota, or uniqueness impact. + +**Failure patterns:** + +```python +# VULNERABLE: retrying the same request can create two charges. +@app.route('/api/v1/payments', methods=['POST']) +@require_auth +def create_payment(): + charge = payment_gateway.charge(current_user.id, request.json['amount']) + Payment.create(user_id=current_user.id, gateway_id=charge.id) + return jsonify({"payment_id": charge.id}), 201 +``` + +```javascript +// VULNERABLE: duplicate webhook event IDs are not stored or rejected. +app.post('/webhooks/provider', async (req, res) => { + await fulfillOrder(req.body.order_id); + res.sendStatus(204); +}); +``` + +```graphql +# VULNERABLE: mutation has no idempotency key, nonce, or version check. +mutation { + approveInvoice(invoiceId: "inv_123") +} +``` + +**Evidence to require:** + +- [ ] High-impact state-changing operations require an idempotency key, event ID, + nonce, version check, or equivalent duplicate control. +- [ ] Replay controls are bound to actor, tenant, operation, and payload hash. +- [ ] Duplicate detection is atomic and durable across replicas, queues, retries, + and failover paths. +- [ ] Retrying the same valid request returns the original result or a conflict, + not a second side effect. +- [ ] Webhook/event IDs are stored with a replay window and rejected when reused. +- [ ] Nonces, timestamps, and signatures have bounded replay windows. +- [ ] Balance, inventory, quota, approval, and uniqueness-sensitive operations + include concurrency tests or transaction evidence. +- [ ] Duplicate/replay rejects and retry storms are logged and alerted. + +**Finding IDs:** + +| ID | Finding | +|---|---| +| API-REPLAY-01 | State-changing operation lacks idempotency key, event ID, nonce, or equivalent duplicate control | +| API-REPLAY-02 | Idempotency key or nonce is not bound to actor, tenant, operation, and payload hash | +| API-REPLAY-03 | Duplicate detection is non-atomic across replicas, queues, retries, or failover paths | +| API-REPLAY-04 | Retry returns a second side effect instead of original result, conflict, or replay rejection | +| API-REPLAY-05 | Webhook or async event handler accepts duplicate event IDs without durable replay tracking | +| API-REPLAY-06 | Replay window for signatures, timestamps, or nonces is missing or too broad | +| API-REPLAY-07 | Balance, inventory, quota, approval, or uniqueness-sensitive operation lacks concurrency evidence | +| API-REPLAY-08 | Duplicate/replay rejects and retry storms are not logged or alerted | + --- ## API7:2023 -- Server Side Request Forgery (SSRF)