---
key: "abuse-case-library"
title: "Abuse case library: tests and controls by feature"
url: "https://softwaresecurity.com/domains/appsec/threat-modeling/abuse-case-library/"
api: "https://softwaresecurity.com/api/v1/articles/abuse-case-library.json"
domain: "appsec"
topic: "Application security"
type: "Reference"
identifiers: ["CWE-307","CWE-204","CWE-640","CWE-639","CWE-862","CWE-915","CWE-434","CWE-22","CWE-362","CWE-841","CWE-345","CWE-918","CWE-1236","CWE-1427","A01:2025","A06:2025","A07:2025","API1:2023","API5:2023","API6:2023","API7:2023","LLM01:2026","LLM03:2026","CAPEC-600","CAPEC-565","CAPEC-575","CAPEC-50","CAPEC-650","CAPEC-126","CAPEC-26","CAPEC-664","CAPEC-66","CAPEC-87","CAPEC-62"]
tags: ["Business logic and race conditions","Threat modeling","Security testing","Secure development lifecycle"]
stages: ["design","test"]
roles: ["developer","appsec-engineer"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Abuse case library: tests and controls by feature

Recurring abuse cases for sign-up and login, password reset, MFA enrolment, invites, file upload, payments and coupons, webhooks, search, export, admin and AI chat or agents: the attacker goal, preconditions, a concrete test and the control, each linked to CWE and CAPEC.

Canonical URL: https://softwaresecurity.com/domains/appsec/threat-modeling/abuse-case-library/

## In short

For each feature, ask what a hostile user wants from it, what they need first, how you would test that they cannot get it and which control stops them. The tables below answer those questions for eleven common features, with a test you can run as an integration test and CWE and CAPEC IDs for each case. Copy the rows that apply into the story, turn them into requirements with acceptance criteria, and keep the tests in the normal suite.

- Do write each abuse case as a test someone else can run: a request, an input and an observable expected result.
- Do include business-logic abuse (coupon races, skipped steps, price tampering); scanners rarely find it.
- Do test authorization features such as invites and exports with two tenants and two roles, not one admin account.
- Do treat every document and tool result an AI feature reads as attacker input, and test the tool gateway, not the prompt.
- Don't stop at the library: add the abuse cases specific to your product, and record each one with an owner and a test.

## Verify it

1. For each feature in your product, list which rows of this library apply and link the test that covers each one; a row without a test is an open item.
2. Run the abuse-case tests against a branch with the control removed and confirm each one fails.
3. Check that authorization abuse cases are tested with two tenants and at least two roles.
4. Confirm every `accepted` abuse case links a dated risk decision with an owner.
5. Review the list after each incident or bug bounty report and add the case that would have caught it.

## What you will learn

- Select the abuse cases that apply to a feature and add product-specific ones.
- Write each abuse case as a runnable test with an observable expected result.
- Name the control and the CWE and CAPEC entries for each case.
- Automate race and replay abuse cases so they run on every change.

Audience: Developers, testers and AppSec engineers writing stories, tests and threat models for product features

Before you start:

- Know how the feature you are working on handles accounts, tenants and roles.
- Be able to read a short Python test.
- For the code: Python 3.11 or later with pytest.

Time: About 39 minutes: 17 to read and 22 for the exercise

## How do you use this library?

An abuse case (also called a misuse case) describes how someone could use a feature against its owner or its other users: what they want, what they need first, and how you would notice. This library lists the abuse cases that recur for common features, each with a concrete test and the control that should make the test pass. The CWE and CAPEC IDs name the weakness and the attack pattern so you can search scanners, advisories and the CWE pages on this site.

Use it in refinement, before code exists. Pick the features the story touches, copy the rows that apply into the story, and turn each into a requirement with acceptance criteria as described in /domains/appsec/threat-modeling/security-requirements-definition-of-done/. Add the abuse cases that are specific to your product: the library covers what recurs, not your business logic. For threats at the design level rather than one feature, use /domains/appsec/threat-modeling/threat-pattern-library/.

- Write the test as a request and an expected result that someone else can run, including the case that must be refused.
- Keep the expected result observable: a status code, a body, a log entry or an unchanged database row.
- Record each abuse case with its owner and status in the YAML template near the end of this page, and link the test that proves it.
- Re-run the abuse-case tests on every release; most of them are cheap integration tests.

## Sign-up and login

Attackers go after login at scale with credentials leaked elsewhere, and after sign-up to learn who has an account or to create accounts in bulk. Deeper guidance: /domains/identity-crypto/authentication/session-lifecycle/, /domains/identity-crypto/authentication/password-storage/ and /domains/identity-crypto/authentication/mfa-account-takeover/.

| Abuse case and attacker goal | Preconditions | Test | Control | CWE / CAPEC |
| --- | --- | --- | --- | --- |
| AC-AUTH-1 Credential stuffing: take over accounts whose owners reused a leaked password | Login endpoint reachable from the internet; lists of leaked email and password pairs | Replay 200 known-wrong pairs for test accounts from many source addresses; expect per-account throttling or a challenge after the documented threshold and an alert | Per-account and per-source rate limits, breached-password checks at sign-up and change (v5.0.0-6.2.12), MFA, alerting (v5.0.0-6.3.1) | CWE-307 / CAPEC-600 |
| AC-AUTH-2 Password spraying: try one common password across many accounts to stay under per-account limits | Usernames or emails can be guessed or collected | Try one password against 50 test accounts at one attempt per account per minute; expect detection on the global failure rate | Global and per-source failure-rate limits, a blocklist of the most common passwords (v5.0.0-6.2.4), MFA | CWE-307 / CAPEC-565 |
| AC-AUTH-3 Account enumeration: learn which emails have accounts | Sign-up, login or reset responses differ for known and unknown emails | Compare status, body and response time for a registered and an unregistered email on sign-up, login and reset; expect no observable difference | Same response for both cases; tell the real owner by email instead of on screen (v5.0.0-6.3.8) | CWE-204 / CAPEC-575 |
| AC-AUTH-4 Bulk fake sign-ups: create thousands of accounts for spam or free-tier abuse | Sign-up grants useful privileges before email verification | Script 500 sign-ups in 10 minutes from one network range; expect throttling and no privileges before verification | Rate limits and anti-automation on sign-up (v5.0.0-2.4.1), verification before privileges, per-tenant quotas | CWE-799 |

## Password reset

Reset is a second login path, and attackers use it when the first one is strong. The reset-token lab at /labs/recovery/ has you make a recovery credential single-use; the guide is /domains/identity-crypto/authentication/account-recovery-linking/.

| Abuse case and attacker goal | Preconditions | Test | Control | CWE / CAPEC |
| --- | --- | --- | --- | --- |
| AC-RST-1 Guess or reuse a reset token to set the victim's password | Tokens are short, predictable, long-lived or reusable | Request two tokens and check they come from a CSPRNG with at least 128 bits; use one token twice and once after expiry; expect both later uses to fail | Random single-use token, stored hashed, expiring in minutes, invalidated when a new one is issued | CWE-640 / CAPEC-50 |
| AC-RST-2 Reset link poisoning: receive the victim's token through a link that points to an attacker host | The app builds the link from the request's `Host` or `X-Forwarded-Host` header | Request a reset with `Host: evil.example`; expect the email link to use the configured origin | Build links from a configured base URL; reject unknown `Host` values at the edge | CWE-640 |
| AC-RST-3 Bypass MFA by resetting the password | Reset signs the user in, or resets the second factor too | Reset the password of an MFA-enrolled test user; expect the next sign-in to still require the second factor | Reset changes only the password and never skips enabled MFA (v5.0.0-6.4.3) | CWE-288 |
| AC-RST-4 Keep access after the victim resets: an attacker's existing session survives | Sessions are not tied to credential changes | Sign in on client A, reset on client B, then call the API from A; expect A to be signed out or offered for termination | Offer to end other sessions after any factor change (v5.0.0-7.4.3); end them by default after a reset | CWE-613 |

## MFA enrolment

An attacker who briefly holds a session tries to make that access permanent by adding their own factor, or tries to wear the user down. Guidance: /domains/identity-crypto/authentication/passkeys-mfa-lifecycle/ and /domains/identity-crypto/authentication/mfa-account-takeover/.

| Abuse case and attacker goal | Preconditions | Test | Control | CWE / CAPEC |
| --- | --- | --- | --- | --- |
| AC-MFA-1 Enroll the attacker's own authenticator on a hijacked session | Enrolment needs only a valid session cookie | Call the enrolment endpoint with a session older than the re-authentication window; expect a step-up prompt and a notification to the user | Full re-authentication before changing authentication factors (v5.0.0-7.5.1); notify on factor changes | CWE-306 |
| AC-MFA-2 Push bombing: send prompts until the user approves one | Push approval without number matching or rate limits | Trigger 20 push prompts in a minute for a test account; expect rate limiting and number matching | Rate-limit push requests (v5.0.0-6.6.4), number matching, alert on repeated denials | CWE-799 |
| AC-MFA-3 Brute-force a six-digit one-time code | No limit on wrong codes per challenge | Submit 50 wrong codes for one challenge; expect the challenge to be invalidated well before that | Rate limiting on code entry (v5.0.0-6.6.3), short code lifetime (v5.0.0-6.5.5) | CWE-307 / CAPEC-112 |
| AC-MFA-4 Reuse a recovery code or a captured one-time code | Codes stay valid after use | Use one recovery code twice and one TOTP code twice within its window; expect the second use to fail | One-time codes and lookup secrets usable once (v5.0.0-6.5.1); recovery codes stored hashed | CWE-294 |

## Invites

Invites grant membership, so they are an authorization feature. Test who can send them, which tenant and role they grant, and how long the link works. Guidance: /domains/appsec/authorization/function-property-authorization/ and /domains/appsec/authorization/object-level-authorization/.

| Abuse case and attacker goal | Preconditions | Test | Control | CWE / CAPEC |
| --- | --- | --- | --- | --- |
| AC-INV-1 A member who is not an admin sends invites by calling the API directly | The button is hidden in the UI but the endpoint is open | Call `POST /invites` as a plain member; expect 403 | Function-level authorization on the endpoint (v5.0.0-8.2.1) | CWE-862 / CAPEC-1 |
| AC-INV-2 Invite someone into another tenant by changing `tenant_id` | The tenant comes from the request body | Send an invite as an admin of tenant red with `tenant_id` of tenant blue; expect 403 or the field ignored | Take the tenant from the session (v5.0.0-8.3.1); cross-tenant checks (v5.0.0-8.4.1) | CWE-639 |
| AC-INV-3 Grant yourself admin by adding `role` to the accept request | The accept handler binds every request field to the membership | Accept an invite with `{"role":"admin"}` added; expect the invited role | Allowlist the fields each action may set (v5.0.0-15.3.3) | CWE-915 |
| AC-INV-4 Use a forwarded or old invite link | Invite tokens never expire and are not bound to the invited email | Accept an invite twice, after 73 hours, and with a different verified email; expect all three refused | Single-use expiring token bound to the invited address | CWE-294 |

## File upload

Uploads let a user put bytes on your servers and in front of other users. Guidance: /domains/appsec/secure-features/secure-file-uploads/ and /domains/appsec/secure-features/archive-path-safety/; fix cards: /fix/file-upload/ and /fix/path-traversal/.

| Abuse case and attacker goal | Preconditions | Test | Control | CWE / CAPEC |
| --- | --- | --- | --- | --- |
| AC-UP-1 Upload a web shell and run it | Uploaded files are stored under the web root and served by an interpreter | Upload `shell.php` renamed to `.jpg` and a polyglot image; request them; expect no execution and a content-type mismatch rejected | Check extension and content (v5.0.0-5.2.2); store outside the web root, never executable (v5.0.0-5.3.1) | CWE-434 / CAPEC-650 |
| AC-UP-2 Write outside the upload directory through the filename | The stored path uses the client filename | Upload with filename `../../app/settings.py`; expect a generated name and no file outside the directory | Generate storage names; never use user-supplied paths (v5.0.0-5.3.2) | CWE-22 / CAPEC-126 |
| AC-UP-3 Exhaust disk or memory with a decompression bomb | Archives or images are expanded without limits | Upload a small archive that expands past the limit; expect rejection before full extraction | Check uncompressed size and file count before extracting (v5.0.0-5.2.3) | CWE-409 |
| AC-UP-4 Store script in an SVG or HTML file and run it in other users' browsers | Uploads are served inline from the application origin | Upload an SVG containing a script and open its URL; expect download or a sandboxed origin, not execution | Serve uploads from a separate origin or with `Content-Disposition: attachment` and a restrictive CSP | CWE-79 |

## Payments and coupons

Money flows reward attackers directly, and most of the bugs are business logic rather than injection. Guidance: /domains/appsec/business-logic/transaction-races/ and /domains/appsec/business-logic/business-logic-flaws/; lab: /labs/checkout-race/.

| Abuse case and attacker goal | Preconditions | Test | Control | CWE / CAPEC |
| --- | --- | --- | --- | --- |
| AC-PAY-1 Redeem a single-use coupon many times with parallel requests | Check and update happen in separate statements | Send 20 redemptions of one coupon at the same instant, repeated over several rounds; expect exactly one success per round | One conditional update that both checks and claims (v5.0.0-15.4.2); transactions and locking for limited resources (v5.0.0-2.3.3, v5.0.0-2.3.4) | CWE-362, CWE-837 / CAPEC-26 |
| AC-PAY-2 Pay less by changing the price or quantity in the request | The client sends price, discount or quantity and the server trusts it | Submit an order with `price: 0.01` and one with `quantity: -1`; expect the server price and a validation error | Compute price server-side from the catalog; validate ranges (v5.0.0-2.2.1) | CWE-472, CWE-602 |
| AC-PAY-3 Skip the payment step and confirm an unpaid order | Each step is a separate endpoint without state checks | Call `POST /orders/{id}/confirm` for an order whose payment failed; expect 409 | Enforce step order on the server (v5.0.0-2.3.1) | CWE-841 |
| AC-PAY-4 Card testing: validate stolen cards with many small authorizations | Payment endpoint without velocity limits | Attempt 100 low-value authorizations from one account and one network range; expect throttling and review | Velocity limits per account, card and source; bot controls on payment endpoints | CWE-799 |

## Webhooks

Receiving webhooks means trusting a request that claims to come from a partner; sending them means fetching URLs your customers choose. Guidance: /domains/appsec/secure-features/verify-webhook-deliveries/ and /domains/appsec/secure-features/ssrf-egress-boundaries/; lab: /labs/webhooks/; fix card: /fix/ssrf/.

| Abuse case and attacker goal | Preconditions | Test | Control | CWE / CAPEC |
| --- | --- | --- | --- | --- |
| AC-WH-1 Forge a "payment succeeded" delivery | The receiver accepts unsigned or wrongly signed deliveries | Post a delivery with no signature, a wrong signature and a changed body; expect all rejected | Verify the provider's HMAC over the raw body in constant time before parsing | CWE-345 |
| AC-WH-2 Replay a captured valid delivery to fulfil an order twice | No timestamp tolerance or delivery-ID check | Resend a valid delivery, and one signed hours ago; expect `replay` and `stale` | Check the signed timestamp against a tolerance and store delivery IDs; make handlers idempotent | CWE-294 |
| AC-WH-3 Make your server call internal addresses by registering a webhook URL | Customers can register any URL; the sender follows redirects | Register `http://169.254.169.254/`, `http://localhost:8080/` and a public URL that redirects inside; expect every delivery refused | Resolve and check the address at send time, block private ranges, do not follow redirects (v5.0.0-15.3.2), send through an egress proxy | CWE-918 / CAPEC-664 |

## Search

Search builds queries from user input and often runs against an index that has its own copy of the data, outside the main access checks. Guidance: /domains/appsec/secure-coding/sql-injection-python-sqlite3/, /domains/appsec/secure-coding/nosql-ldap-xpath-injection/ and /domains/appsec/input-handling/redos-input-validation/.

| Abuse case and attacker goal | Preconditions | Test | Control | CWE / CAPEC |
| --- | --- | --- | --- | --- |
| AC-SRCH-1 Change the query logic through the search term or filters | Queries are built by string concatenation, or filter objects are passed to the database as received | Search for `' OR 1=1--` and send a filter of `{"$ne": null}`; expect literal matching and validation errors | Parameterized queries; allowlisted filter fields and operators | CWE-89, CWE-943 / CAPEC-66 |
| AC-SRCH-2 Find another tenant's records through the index | The index is shared and the tenant filter is applied after ranking, or not at all | Search from tenant red for a word that appears only in tenant blue's documents; expect no results, counts or snippets | Apply the tenant and permission filter inside the index query; test it like an API authorization rule | CWE-863 |
| AC-SRCH-3 Make search hang with an expensive pattern | User input reaches a backtracking regex engine or unbounded wildcard query | Submit a pattern with nested quantifiers and a long near-match; expect a fast rejection or timeout | Linear-time regex engines or input limits; query timeouts and cost limits | CWE-1333 / CAPEC-492 |

## Export

Exports move a lot of data in one request and are often added late, with weaker checks than the screens they mirror. Guidance: /domains/appsec/authorization/object-level-authorization/ and /domains/runtime/api-security/resource-consumption-limits/; lab: /labs/authorization/.

| Abuse case and attacker goal | Preconditions | Test | Control | CWE / CAPEC |
| --- | --- | --- | --- | --- |
| AC-EXP-1 Export another tenant's data by changing the ID | Export jobs check the job ID but not the data it covers | Request an export of a record set owned by another tenant; expect 404 | Authorize the data, not only the job (v5.0.0-8.2.2) | CWE-639 |
| AC-EXP-2 Plant a spreadsheet formula that runs when an admin opens the export | User text is written to CSV without neutralization | Store `=HYPERLINK("https://evil.example","x")` in a name and export; expect the cell prefixed so it is shown as text | Prefix cells starting with `=`, `+`, `-`, `@`, tab or carriage return; quote fields | CWE-1236 |
| AC-EXP-3 Scrape everything through repeated bulk exports | No limits on export size or frequency | Run exports in a loop; expect a per-user limit and an alert | Per-user and per-tenant quotas; asynchronous exports with size caps (v5.0.0-2.4.1) | CWE-770 |
| AC-EXP-4 Download someone's export through a leaked link | Download URLs are public and long-lived | Open an export link from another session and after 24 hours; expect refusal | Short-lived signed URLs bound to the requesting user; delete exports after use | CWE-552 |

## Admin functions

Admin features are few, powerful and often tested less than customer features. Guidance: /domains/appsec/authorization/function-property-authorization/ and /domains/appsec/browser-security/csrf-origin-defense/; fix cards: /fix/missing-authorization/, /fix/mass-assignment/ and /fix/csrf/.

| Abuse case and attacker goal | Preconditions | Test | Control | CWE / CAPEC |
| --- | --- | --- | --- | --- |
| AC-ADM-1 Call admin APIs as an ordinary user | Admin routes rely on the UI hiding them | Call each `/admin` route as a member; expect 403 everywhere, including export and bulk routes | Deny-by-default function-level checks (v5.0.0-8.2.1); a route inventory test | CWE-862 / CAPEC-87 |
| AC-ADM-2 Promote yourself by adding `role` to a profile update | Profile updates bind all fields | Send `PATCH /users/me` with `{"role":"admin"}`; expect the role unchanged | Field allowlists per action (v5.0.0-15.3.3); field-level authorization (v5.0.0-8.2.3) | CWE-915 |
| AC-ADM-3 Get an admin to change a role through a forged cross-site request | Cookie-authenticated admin actions without CSRF defense | Submit the role change from another origin with the admin's cookies; expect rejection | SameSite cookies plus a token or `Origin` check on every state-changing request | CWE-352 / CAPEC-62 |
| AC-ADM-4 Impersonate a customer without leaving a trace | Support impersonation is not logged or not visible to the customer | Impersonate a test user; expect an audit event with who, whom and when, and a notice to the customer | Log every impersonation and admin change to a protected log (v5.0.0-16.4.2); require a reason | CWE-778 |

## AI chat or agent

An LLM feature takes instructions from anything it reads, so every document, web page and tool result is input from an attacker. Guidance: /domains/ai-emerging/llm-security/prompt-injection-boundaries/, /domains/ai-emerging/agent-security/tool-authorization/ and /domains/ai-emerging/llm-security/rag-tenant-isolation/; labs: /labs/agent-tool-gateway/, /labs/rag-isolation/ and /labs/output-rendering/.

| Abuse case and attacker goal | Preconditions | Test | Control | CWE / CAPEC |
| --- | --- | --- | --- | --- |
| AC-AI-1 Indirect prompt injection: a shared document tells the assistant to send data to an outside URL | The assistant can read untrusted content and call an outbound tool | Plant instructions in a test document, ask the assistant to summarize it, and watch egress; expect no outbound call and no data in tool arguments | Tool gateway outside the model that checks each call against the user's intent and an allowlist; no free-form fetch tool | CWE-1427 |
| AC-AI-2 Excessive agency: the agent deletes or pays without a human decision | Destructive tools are callable without approval | Ask the agent to delete a project and to issue a refund; expect an approval bound to the exact arguments | Approval bound to tool, arguments and user, consumed once (the agent tool gateway lab) | CWE-862 |
| AC-AI-3 Retrieve another tenant's documents through the assistant | Vector search runs without the tenant filter | Ask tenant red's assistant about a fact only in tenant blue's documents; expect no answer from them | Apply tenant and permission filters inside retrieval, before ranking | CWE-639 |
| AC-AI-4 Make the model output markup that runs in the user's browser | Model output is rendered as HTML | Ask for an answer containing an `<img onerror>` payload; expect it shown as text | Render output as text or sanitize with an allowlist; CSP | CWE-79 |
| AC-AI-5 Run up the bill with long prompts and loops | No per-user token or tool-call budget | Send prompts that trigger repeated tool calls; expect the run to stop at the budget | Per-user and per-run budgets for tokens, tool calls and time | CWE-770 |

## How do you record an abuse case?

Store each abuse case next to the story or feature it belongs to, in a format both people and scripts can read. The record names the attacker and their goal, the preconditions, the test and where it lives, the control, the mappings and an owner. A status of `tested` should mean a test in the repository proves it; `accepted` should point to a dated risk decision.

```yaml
# One abuse case, stored next to the story it belongs to (for example docs/abuse-cases/AC-PAY-1.yaml).
id: AC-PAY-1
feature: checkout / coupons
title: Redeem a single-use coupon more than once
attacker:
  who: Signed-in customer with one valid single-use coupon
  goal: Apply the coupon to many orders
preconditions:
  - Coupon redemption is an HTTP request the client can send in parallel
  - The server checks "unused" and marks "used" in separate steps
test:
  type: automated
  steps: Send 20 redemption requests for the same coupon at the same moment, each for a different order
  expected: Exactly one succeeds; the others get 409 and no discount
  location: tests/test_abuse_cases.py::test_ac_pay_1_single_use_coupon_under_parallel_requests
control: A single conditional UPDATE ... WHERE used_by IS NULL, checked by its affected row count
mappings:
  cwe: [CWE-362, CWE-837]
  capec: [CAPEC-26]
  asvs: [v5.0.0-2.3.3, v5.0.0-2.3.4, v5.0.0-15.4.2]
owner: payments team
status: tested
```

## What do the coupon and webhook controls look like in code?

Two controls from the tables, in Python with only the standard library. `redeem` claims the coupon with one conditional `UPDATE` and trusts the affected row count, so the check and the claim cannot be separated by another request. `WebhookVerifier` checks an HMAC over the delivery ID, the timestamp and the raw body with a constant-time comparison, then rejects stale timestamps and delivery IDs it has already seen. The signing scheme is an example; use your provider's documented scheme.

```python
import hashlib
import hmac
import sqlite3
import time


def redeem(db_path, code, order_id):
    """AC-PAY-1 control: one conditional UPDATE, so only one request can win."""
    with sqlite3.connect(db_path, timeout=10, isolation_level=None) as db:
        won = db.execute(
            "UPDATE coupons SET used_by = ? WHERE code = ? AND used_by IS NULL",
            (order_id, code),
        ).rowcount
    return won == 1


class WebhookVerifier:
    """AC-WH-1 and AC-WH-2 controls: verify the HMAC, the timestamp and the delivery ID."""

    def __init__(self, secret, tolerance=300, clock=time.time):
        self.secret, self.tolerance, self.clock = secret, tolerance, clock
        self.seen = {}  # delivery id -> timestamp; use a shared store with a TTL in production

    def verify(self, delivery_id, timestamp, body, signature):
        expected = hmac.new(
            self.secret, f"{delivery_id}.{timestamp}.".encode() + body, hashlib.sha256
        ).hexdigest()
        if not hmac.compare_digest(expected, signature):
            return "bad-signature"
        if abs(self.clock() - int(timestamp)) > self.tolerance:
            return "stale"
        if delivery_id in self.seen:
            return "replay"
        self.seen[delivery_id] = int(timestamp)
        return "ok"
```

## How do you automate abuse-case tests?

Each test performs the abuse case and asserts the refusal. The coupon test releases 20 threads at once through a barrier and repeats the race for 25 rounds, because a race does not appear on every run. Against a version that reads the coupon and then updates it in a second statement, the test failed in five of five runs during testing; against the conditional update it passed every time. The webhook tests use a fixed clock, so they are deterministic.

Run them with `pytest -q` (tested with Python 3.14 and pytest 9.1.1). Keep abuse-case tests in the normal suite, so they run on every change rather than once before a penetration test.

```python
import hashlib
import hmac
import sqlite3
import threading
from concurrent.futures import ThreadPoolExecutor

import pytest

from abuse_cases import WebhookVerifier, redeem


@pytest.fixture
def coupon_db(tmp_path):
    path = tmp_path / "shop.db"
    with sqlite3.connect(path) as db:
        db.execute("CREATE TABLE coupons (code TEXT PRIMARY KEY, used_by TEXT)")
        db.execute("INSERT INTO coupons VALUES ('WELCOME10', NULL)")
    return path


def test_ac_pay_1_single_use_coupon_under_parallel_requests(coupon_db):
    # A race may not show on every run, so repeat it; any double redemption fails the test.
    for round_ in range(25):
        with sqlite3.connect(coupon_db) as db:
            db.execute("UPDATE coupons SET used_by = NULL")
        start = threading.Barrier(20)

        def attempt(n):
            start.wait()  # release all 20 requests at once
            return redeem(coupon_db, "WELCOME10", f"order-{round_}-{n}")

        with ThreadPoolExecutor(max_workers=20) as pool:
            results = list(pool.map(attempt, range(20)))
        assert results.count(True) == 1, f"round {round_}: {results.count(True)} redemptions"


SECRET = b"whsec_test_only"


def sign(delivery_id, timestamp, body):
    message = f"{delivery_id}.{timestamp}.".encode() + body
    return hmac.new(SECRET, message, hashlib.sha256).hexdigest()


@pytest.fixture
def verifier():
    return WebhookVerifier(SECRET, clock=lambda: 1_760_000_000)


def test_ac_wh_1_forged_delivery_is_rejected(verifier):
    body = b'{"order":"o-7","status":"paid"}'
    assert verifier.verify("d-1", "1760000000", body, "0" * 64) == "bad-signature"


def test_ac_wh_1_changed_body_is_rejected(verifier):
    signature = sign("d-1", "1760000000", b'{"order":"o-7","status":"failed"}')
    assert verifier.verify("d-1", "1760000000", b'{"order":"o-7","status":"paid"}', signature) == "bad-signature"


def test_ac_wh_2_replayed_delivery_is_rejected(verifier):
    body = b'{"order":"o-7","status":"paid"}'
    signature = sign("d-2", "1760000000", body)
    assert verifier.verify("d-2", "1760000000", body, signature) == "ok"
    assert verifier.verify("d-2", "1760000000", body, signature) == "replay"


def test_ac_wh_2_old_delivery_is_rejected(verifier):
    body = b'{"order":"o-7","status":"paid"}'
    signature = sign("d-3", "1759990000", body)  # 10,000 seconds old
    assert verifier.verify("d-3", "1759990000", body, signature) == "stale"
```

## Guided practice

A team is adding "Share a report by link" to a fictional analytics SaaS. Use the library to find the abuse cases that apply, write two of them as tests and decide which controls the design needs. The product and data are fictional.

### Supplied material

```text
Feature: report sharing (story RPT-12)
- An editor clicks Share and gets a link: https://app.example.test/r/{report_id}?k={key}
- report_id is a sequential integer; k is 8 hex characters from Math.random()
- Anyone with the link can view the report without signing in; links never expire
- Viewers can click "Download CSV"; cell values come from customer-uploaded data
- Editors can share; viewers cannot (the Share button is hidden for viewers)
- POST /reports/{id}/share accepts {"expires_in_days": n, "role": "viewer"}
Current tests: one happy-path test that an editor can create a link.
```

### 1. Find the applicable rows

Pick the library rows that apply to this feature and say why.

Worked answer: AC-EXP-4 (leaked, long-lived link), AC-EXP-1 (guessable `report_id` with a weak key is the same object-access problem), AC-EXP-2 (CSV formula injection from customer data), AC-INV-1 (viewers calling the share endpoint directly) and AC-INV-3 (a `role` field in the request that could grant more than view).

### 2. Judge the key

Is `k` a sufficient control for AC-EXP-1 and AC-EXP-4?

Worked answer: No. Eight hex characters are 32 bits from a non-cryptographic generator, and the link never expires. Use at least 128 bits from a CSPRNG, store it hashed, give it an expiry and allow revocation (CWE-552, CWE-330 for the weak randomness).

### 3. Write the authorization test

Write the test for AC-INV-1 in the format of the library.

Worked answer: As a viewer, call `POST /reports/{id}/share`; expect 403 and no link created. Add the same test for an editor of another workspace.

### 4. Write the export test

Write the test for AC-EXP-2.

Worked answer: Upload a data row whose value is `=HYPERLINK("https://evil.example","x")`, download the CSV and assert the cell starts with an apostrophe or is otherwise neutralized; also test `+`, `-`, `@` and a leading tab.

### 5. Close the role field

Decide what the share endpoint should do with the `role` field.

Worked answer: Allowlist it to `viewer` (or the roles an editor may grant), and add a test that `"role": "editor"` or `"admin"` is refused (CWE-915).

Deliverable: A list of five abuse cases for RPT-12 with tests and controls, and two test descriptions ready to automate.

Scope: The feature is fictional and simplified. Mappings choose the closest CWE; your scanner may map the same issue to a parent or child entry.

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [OWASP Abuse Case Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Abuse_Case_Cheat_Sheet.html)
- [MITRE CAPEC: Mechanisms of Attack view (CAPEC-1000)](https://capec.mitre.org/data/definitions/1000.html). CAPEC 3.9.
- [MITRE CWE: Research Concepts view (CWE-1000)](https://cwe.mitre.org/data/definitions/1000.html). CWE 4.20.
- [OWASP ASVS 5.0.0: V6 Authentication](https://github.com/OWASP/ASVS/blob/v5.0.0_release/5.0/en/0x15-V6-Authentication.md)
- [OWASP ASVS 5.0.0: V2 Validation and Business Logic](https://github.com/OWASP/ASVS/blob/v5.0.0_release/5.0/en/0x11-V2-Validation-and-Business-Logic.md)
- [OWASP ASVS 5.0.0: V5 File Handling](https://github.com/OWASP/ASVS/blob/v5.0.0_release/5.0/en/0x14-V5-File-Handling.md)
- [OWASP Web Security Testing Guide: Business Logic Testing](https://wstg.owasp.org/v4.2/4-Web_Application_Security_Testing/10-Business_Logic_Testing/). WSTG v4.2.
- [OWASP CSV Injection](https://community.owasp.org/attacks/CSV_Injection)
- [OWASP Top 10 for LLM Applications](https://genai.owasp.org/llm-top-10/)