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.
- 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.
- Run the abuse-case tests against a branch with the control removed and confirm each one fails.
- Check that authorization abuse cases are tested with two tenants and at least two roles.
- Confirm every
acceptedabuse case links a dated risk decision with an owner. - Review the list after each incident or bug bounty report and add the case that would have caught it.
- Applies to
- OWASP ASVS 5.0.0
- CWE and CAPEC CWE list as of October 2026; CAPEC 3.9
- Examples Python 3.14, pytest 9.1.1, SQLite 3 (tested)
Web and API products with accounts, tenants, payments, integrations or AI features. Controls cite OWASP ASVS 5.0.0 requirements where one applies; the examples use Python 3.14 and pytest 9.1.1.
- Part of
- Workflow: Review a design
Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.
Learning goals and prerequisites
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.
Before you start
Developers, testers and AppSec engineers writing stories, tests and threat models for product features
- 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.
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.
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 |
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 |
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 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 |
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 |
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 |
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 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 |
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 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 |
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 |
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.
# 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: testedTwo 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.
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"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.
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 scenario material
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.
Expected result and reasoning
AC-EXP-4 (leaked, long-lived link), AC-EXP-1 (guessable
report_idwith 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 (arolefield in the request that could grant more than view).2. Judge the key
Is
ka sufficient control for AC-EXP-1 and AC-EXP-4?Expected result and reasoning
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.
Expected result and reasoning
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.
Expected result and reasoning
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
rolefield.Expected result and reasoning
Allowlist it to
viewer(or the roles an editor may grant), and add a test that"role": "editor"or"admin"is refused (CWE-915).
The feature is fictional and simplified. Mappings choose the closest CWE; your scanner may map the same issue to a parent or child entry.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- OWASP Abuse Case Cheat Sheet
- MITRE CAPEC: Mechanisms of Attack view (CAPEC-1000). CAPEC 3.9.
- MITRE CWE: Research Concepts view (CWE-1000). CWE 4.20.
- OWASP ASVS 5.0.0: V6 Authentication
- OWASP ASVS 5.0.0: V2 Validation and Business Logic
- OWASP ASVS 5.0.0: V5 File Handling
- OWASP Web Security Testing Guide: Business Logic Testing. WSTG v4.2.
- OWASP CSV Injection
- OWASP Top 10 for LLM Applications