Writing Abuse Cases for User Stories

An abuse case is a user story with the sign flipped: instead of describing what a legitimate user wants, it describes what someone who benefits from misusing the feature would do, and what they get. It is deliberately the same shape as the artefact the team already works with, because that is what lets it enter refinement, get estimated, and produce acceptance criteria rather than sit in a document nobody opens.

This guide is the practice itself — actors, an adversarial walk through the feature’s own steps, a filter for what actually matters, and the handoff to a testable criterion. It is part of the Security Requirements & Abuse Cases guide within Threat Modeling Fundamentals & Methodology.

Prerequisites

  • A refinement session where stories are discussed before estimation
  • A trigger list stating which stories require this treatment
  • The control baselines for your common feature types, in the repository
  • Someone able to review the first few until the team calibrates

Expected Outcomes

  • Three or four abuse cases on every triggered story, written before estimation
  • Each one naming an actor, a method and a concrete gain
  • Each one either converted to a testable criterion or deliberately discarded
  • The exercise finishing inside ten minutes

Step 1: Name the Actors Who Gain From Misuse

Four actor categories cover nearly every case, and naming them explicitly prevents the failure where every abuse case is about an anonymous external hacker.

Four Actors Worth Considering for Every Feature The outsider has no account and wants access or information. The insider has a legitimate account and wants more than their role permits or data belonging to others. The former user has lost access and wants to keep or regain it. The automated abuser has cheap accounts and wants scale — enumeration, spam, or free consumption of a metered resource. The outsider has: a browser and whatever is public wants: access, or information about who your customers are The insider has: a real account and a legitimate role wants: more than the role permits, or another tenant's data The former user has: knowledge of the system and, briefly, access wants: to keep a way back in after offboarding — an invite, a token, a shared link The automated abuser has: cheap accounts and a script wants: scale — enumeration, spam through your domain, or free use of something metered

Step 2: Walk the Feature’s Own Steps, Adversarially

The most productive technique is also the simplest: take the steps the story already describes and ask, at each one, what someone would do with it that you did not intend.

## Story
As a project owner, I can generate a share link so people outside the workspace can view a report.

## The steps this story implies, walked adversarially
1. Owner requests a link
   → Can a non-owner request one? Can a viewer? Can a departing member?
2. System generates a token
   → Is it guessable? Does it name the resource? Does it expire? Can it be enumerated?
3. Link is sent out of band
   → Where does it end up: chat logs, forwarded mail, a search index?
4. Recipient opens the link
   → Does opening it reveal more than the report — the workspace name, other members, navigation?
5. Owner revokes the link
   → Does revocation take effect immediately, including for anyone currently viewing?
6. Owner leaves the company
   → Do their links survive them?

Six questions, all of which come from the feature’s own description rather than from a security taxonomy. This is why engineers write better abuse cases than external reviewers: they know which of those steps has a shortcut in the implementation.


Step 3: Keep the Ones With a Real Gain

Not every adversarial thought is worth a criterion. Filter on what the attacker actually gets.

## Kept — a real gain, worth a criterion
1. Outsider guesses or receives a forwarded link and reads a report they were never granted.
   → Gain: confidential financial data. Criterion: 128-bit token, expiry, revocable, no enumeration.
2. Departing owner's links keep working after offboarding.
   → Gain: persistent access to live data. Criterion: links revoked when the creator loses access.
3. Automated abuser enumerates tokens to harvest reports.
   → Gain: bulk data. Criterion: rate limit per address, and a uniform response for invalid tokens.
4. Viewer discovers workspace membership from the share page.
   → Gain: a customer's staff list. Criterion: the page renders only the report and nothing else.

## Discarded — no meaningful gain
- "Owner shares a link with someone they should not have."  → a business decision, not a control.
- "Recipient screenshots the report."  → outside the trust boundary; document, do not mitigate.

Writing down the discarded cases is worth the extra minute. It shows a later reviewer that the question was asked and answered, which is exactly the difference between a considered scope and an oversight.

Why a Case Was Discarded Is Worth One Line Three legitimate reasons to discard: the attacker gains nothing worth having, the concern is a business decision rather than a control, or the behaviour sits outside the trust boundary entirely. Each recorded in a line, so a later reviewer can see the question was asked and answered rather than missed. No meaningful gain the attacker ends up with something they could already obtain legitimately A business decision, not a control "the owner shares a link with the wrong person" is a policy question for the product Outside the trust boundary "the recipient screenshots the report" — document the limit, do not pretend to mitigate it

Step 4: Hand Each One to a Criterion

## Acceptance criteria (security)
- [ ] Share tokens are 128-bit random and are not derived from the report identifier.
- [ ] Tokens expire after 30 days by default; the owner may choose a shorter period, never a longer one.
- [ ] Revoking a link takes effect on the next request, including for sessions already viewing.
- [ ] All links created by a member are revoked when that member loses workspace access.
- [ ] Invalid and expired tokens return the same response, with no timing difference.
- [ ] The share page renders the report only — no workspace name, member list or navigation.
- [ ] Token lookups are rate limited to 60 per address per hour.

If a kept abuse case produces no criterion anyone can test, one of two things is true: the mitigation is a process rather than a control, or the case was not as concrete as it looked. Both are worth noticing at refinement rather than at review.

From a Dozen Questions to Four Criteria An adversarial walk generates roughly a dozen questions. Filtering on whether the attacker gains anything worth having keeps three or four. Each kept case becomes one testable acceptance criterion. Cases that produce no testable criterion are either process concerns or were never concrete, and both are worth identifying during refinement. ~12 adversarial questions, from the feature's own steps four actors × the steps the story already describes 3 to 4 kept, filtered on real gain discarded ones are recorded, so the scope decision is visible 3 to 4 testable criteria each one becomes a named test in the suite A case that yields no testable criterion is a process concern or was never concrete — say which, and move on.

Verification

The practice is working when three things are true, and each is observable:

# 1. Triggered stories carry abuse cases before estimation, not after.
python3 tools/audit_tickets.py --window 30d --field abuse_cases --require-before estimate

# 2. Each security criterion maps to a named test.
python3 tools/check_criteria_have_tests.py --tickets .tickets/ --tests tests/

# 3. Some abuse cases produced failing tests before the feature shipped.
git log --since='30 days ago' --grep='abuse case' --oneline | wc -l

The third measure is the meaningful one. A team writing abuse cases that never produce a failing test is documenting what it was going to build anyway; a team whose abuse cases regularly catch something before release is doing the work the practice exists for.


Troubleshooting

Symptom Likely cause Fix
Every abuse case is about an anonymous attacker Actor list not used Walk all four actors explicitly, including the insider and the former user
Abuse cases restate the coding standards Written from a control checklist rather than the feature Start from the story’s own steps, not from a taxonomy
The exercise takes 40 minutes Story too large, or genuinely novel Split the story; escalate novel designs to a proper modelling session
Nothing is ever discarded Filter not applied Require a gain statement per case; no gain, no case
Criteria are written but never tested No link between criterion and test name Add the CI check that maps criteria to tests
Mitigations slip to a later sprint Mitigation split into its own ticket Keep it inside the story estimate unless it is a genuine dependency

Common Implementation Mistakes


Frequently Asked Questions

How long should this take per story?

Ten minutes at refinement, for stories that hit a trigger. If it consistently takes much longer, the story is probably too large or the design genuinely novel — and either is useful information that the exercise has surfaced early. If it consistently takes two minutes and yields nothing, the trigger list is too broad; narrow it until the exercise earns its place in the session.

What if the team has no security expertise?

Abuse cases draw on product knowledge more than security knowledge. The core question — who benefits from misusing this, and how — is best answered by the people who know how the feature actually works. Provide the actor list, a handful of worked examples for your common feature shapes, and a reviewer for the first few sessions. In practice teams reach a usable standard within two or three sprints.

Should abuse cases be estimated as separate work?

The mitigation belongs inside the story estimate. Splitting it into a separate ticket invites exactly the outcome you want to avoid: the feature ships and the mitigation waits for capacity that never arrives. Where a mitigation is genuinely large — introducing an authorization layer that does not yet exist — that is a real dependency deserving its own ticket, and the story should be blocked on it rather than shipped without it.