diff --git a/skills/appsec/api-security/SKILL.md b/skills/appsec/api-security/SKILL.md index cbb125aa..a679d02b 100644 --- a/skills/appsec/api-security/SKILL.md +++ b/skills/appsec/api-security/SKILL.md @@ -51,6 +51,37 @@ 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, async event consumers, queue workers, and job enqueue APIs. + +**Evidence to collect:** + +- Inventory of high-impact state-changing operations, including charges, transfers, approvals, deletes, restores, workflow transitions, inventory changes, quota changes, webhook handlers, job producers, and GraphQL mutations. +- Idempotency key, event ID, nonce, timestamp/signature, version check, or equivalent duplicate-control requirement for each high-impact operation. +- Binding evidence showing replay controls are tied to actor, tenant, operation, resource, and payload hash. +- Atomic duplicate-detection evidence across replicas, queues, retries, and failover paths, such as unique constraints, durable ledgers, compare-and-swap, or transactional outbox records. +- Retry response behavior showing the original result, conflict, or replay rejection instead of a second side effect. +- Replay-window evidence for webhook signatures, nonces, timestamps, and event IDs. +- Logging and alerting evidence for duplicate rejects, replay rejects, retry storms, and concurrency conflicts. + +**What to flag:** + +``` +API-REPLAY-01: State-changing operation lacks idempotency key, event ID, nonce, version check, or equivalent duplicate control +API-REPLAY-02: Idempotency key or nonce is not bound to actor, tenant, operation, resource, 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 +``` + +Map these findings primarily to **API6:2023 -- Unrestricted Access to Sensitive Business Flows** for repeated business actions, and to **API4:2023 -- Unrestricted Resource Consumption** when retries or redelivery create resource exhaustion. Escalate to **High** when duplicate side effects can create charges, transfers, approvals, inventory loss, destructive deletes, or privilege changes. + +--- + ## Findings Classification Each finding produced by this review must include the following fields: @@ -112,6 +143,12 @@ 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 | Replay Window | Logging/Alerting | +|---|---|---|---|---|---|---|---|---| +| [POST /payments] | [REST/GraphQL/Webhook/Queue] | [charge/approval/delete/job] | [key/event/nonce/version] | [actor/tenant/resource/payload] | [unique constraint/ledger/CAS] | [original/conflict/reject] | [duration] | [signals] | + ### Findings #### API-SEC-001: [Title] @@ -215,6 +252,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..89c010e5 100644 --- a/skills/appsec/api-security/api-top10-checklist.md +++ b/skills/appsec/api-security/api-top10-checklist.md @@ -370,6 +370,71 @@ def purchase_ticket(): - [ ] High-value operations require step-up verification. - [ ] Business logic abuse scenarios are documented and monitored. +### Idempotency and Replay Evidence Gate + +Rate limits reduce volume, but they do not prove that retries, duplicate webhook +delivery, 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. + +**Vulnerable examples:** + +```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, resource, 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, version check, or equivalent duplicate control | +| API-REPLAY-02 | Idempotency key or nonce is not bound to actor, tenant, operation, resource, 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) diff --git a/skills/appsec/api-security/tests/benign/idempotent-payment-and-webhook-controls.md b/skills/appsec/api-security/tests/benign/idempotent-payment-and-webhook-controls.md new file mode 100644 index 00000000..5291ee83 --- /dev/null +++ b/skills/appsec/api-security/tests/benign/idempotent-payment-and-webhook-controls.md @@ -0,0 +1,80 @@ +# Benign: Idempotent Payment and Webhook Controls + +## Review Target + +```yaml +api: + style: REST + GraphQL + Webhook + state_changing_operations: + - operation: POST /api/v1/payments + side_effect: create_charge + idempotency_key_required: true + idempotency_key_header: Idempotency-Key + binding: + actor: user_123 + tenant: tenant_a + operation: create_payment + resource: cart_456 + payload_hash: sha256:7d9c + duplicate_detection: + storage: payments_idempotency_ledger + unique_constraint: tenant_id + actor_id + operation + key + atomic: true + cross_replica: true + failover_safe: true + retry_response: original_result + replay_window: "24h" + concurrency_test: "tests/payments/test_idempotent_double_submit.py" + logging: + duplicate_rejects: true + retry_storm_alert: true + - operation: mutation approveInvoice + side_effect: approval_transition + nonce_required: true + version_check: invoice_version_compare_and_swap + binding: + actor: approver_id + tenant: tenant_id + operation: approve_invoice + resource: invoice_id + payload_hash: mutation_hash + retry_response: conflict_on_version_mismatch + concurrency_test: "tests/graphql/test_approve_invoice_race.js" + - operation: POST /webhooks/provider + side_effect: fulfill_order + provider_event_id_stored: true + event_id_store: webhook_event_ledger + signature_timestamp_window: "5m" + duplicate_event_behavior: return_204_noop + queue_redelivery_dedup: true + logging: + replay_rejects: true + +observed_evidence: + mobile_double_tap: + requests: 2 + charges_created: 1 + second_response: original_result + webhook_redelivery: + event_id: evt_999 + deliveries: 3 + fulfillments_created: 1 + duplicate_responses: noop +``` + +## Expected Review Result + +| Gate | Status | Evidence | +|------|--------|----------| +| Operation inventory | Pass | Payment, GraphQL approval, and webhook operations are inventoried. | +| Replay control | Pass | Payment uses idempotency key, GraphQL uses nonce/version check, webhook uses provider event ID. | +| Binding | Pass | Controls bind actor, tenant, operation, resource, and payload hash. | +| Atomicity | Pass | Payment ledger has unique constraint and is replica/failover safe. | +| Retry behavior | Pass | Payment retry returns original result; GraphQL conflict blocks stale approval. | +| Replay window | Pass | Payment window is documented and webhook timestamp window is five minutes. | +| Concurrency evidence | Pass | Payment and GraphQL race tests are referenced. | +| Logging and alerting | Pass | Duplicate rejects, replay rejects, and retry storms are logged or alerted. | + +## Reviewer Notes + +This evidence supports marking the idempotency/replay gate as controlled. Continue monitoring retry storm alerts and ensure idempotency keys are not reused across actors, tenants, operations, resources, or payloads. diff --git a/skills/appsec/api-security/tests/vulnerable/duplicate-payment-and-webhook-replay.md b/skills/appsec/api-security/tests/vulnerable/duplicate-payment-and-webhook-replay.md new file mode 100644 index 00000000..676da90c --- /dev/null +++ b/skills/appsec/api-security/tests/vulnerable/duplicate-payment-and-webhook-replay.md @@ -0,0 +1,67 @@ +# Vulnerable: Duplicate Payment and Webhook Replay + +## Review Target + +```yaml +api: + style: REST + Webhook + state_changing_operations: + - operation: POST /api/v1/payments + side_effect: create_charge + idempotency_key_required: false + event_id_required: false + nonce_required: false + binding: + actor: false + tenant: false + operation: false + resource: false + payload_hash: false + duplicate_detection: + storage: none + atomic: false + cross_replica: false + failover_safe: false + retry_response: creates_new_charge + replay_window: none + concurrency_test: missing + logging: + duplicate_rejects: false + retry_storm_alert: false + - operation: POST /webhooks/provider + side_effect: fulfill_order + provider_event_id_stored: false + signature_timestamp_window: "24h" + duplicate_event_behavior: fulfill_again + queue_redelivery_dedup: false + logging: + replay_rejects: false + +observed_evidence: + mobile_double_tap: + requests: 2 + charges_created: 2 + same_user: user_123 + same_cart: cart_456 + webhook_redelivery: + event_id: evt_999 + deliveries: 3 + fulfillments_created: 3 +``` + +## Expected Findings + +| ID | Severity | Evidence | +|----|----------|----------| +| API-REPLAY-01 | High | Payment operation lacks idempotency key, event ID, nonce, version check, or equivalent duplicate control. | +| API-REPLAY-02 | High | No replay control is bound to actor, tenant, operation, resource, or payload hash. | +| API-REPLAY-03 | High | Duplicate detection has no durable atomic storage and is not safe across replicas or failover. | +| API-REPLAY-04 | High | Retrying the payment creates a second charge instead of original result, conflict, or reject. | +| API-REPLAY-05 | High | Webhook handler accepts the same provider event ID and fulfills the order repeatedly. | +| API-REPLAY-06 | Medium | Signature timestamp replay window is 24 hours with no event-ID deduplication. | +| API-REPLAY-07 | High | Payment and fulfillment paths lack concurrency or transaction evidence. | +| API-REPLAY-08 | Medium | Duplicate/replay rejects and retry storms are not logged or alerted. | + +## Reviewer Notes + +This should be reported under API6 for duplicate business actions and API4 where redelivery can exhaust downstream resources. Require a durable idempotency ledger, event-ID store, actor/tenant/payload binding, bounded replay windows, and retry-safe responses.