Security Requirements & Abuse Case Modeling
Threat models describe systems; backlogs contain features. The gap between the two is where most threat modelling effort quietly evaporates: a document lists forty threats, the team ships a sprint of stories, and nothing in the story text ever mentions any of them. The fix is not more analysis — it is expressing the analysis in the unit of work the team already uses.
This guide covers that translation: abuse cases written beside user stories, acceptance criteria a reviewer can actually check, control baselines for recurring feature shapes so common cases need no fresh thinking, and a definition of done that treats security criteria exactly like functional ones. It is part of Threat Modeling Fundamentals & Methodology, and it is where the output of STRIDE framework implementation becomes work an engineer can pick up.
Threat Anatomy
The failure mode here is organisational rather than technical, and it is remarkably consistent. A threat model is produced at design time, usually by whoever cares most about security. It is thorough, and it is written in the vocabulary of threats and controls. Implementation then proceeds from tickets written in the vocabulary of features and users. Nobody maps one to the other, so the model describes a system nobody is building and the tickets describe work nobody is threat modelling.
Three symptoms tell you this is happening. Threat models are updated on a schedule rather than when the architecture changes. Security findings arrive from penetration tests describing things the model already predicted. And engineers describe security as “the review at the end” rather than as part of the work — which is an accurate description of the process they have been given.
Prerequisites & Scope
- A backlog with a definition of done the team actually applies, rather than one on a wiki page.
- A threat model or at least a documented set of trust boundaries, so abuse cases have something to draw on — see defining trust boundaries.
- A test suite that runs on every change, because untested criteria decay within a quarter.
- Someone who can write baselines, usually whoever reviews security work today.
Out of scope: compliance evidence generation, covered by threat model documentation patterns, and the scoring that decides which threats matter most, covered by threat prioritization and risk scoring.
Mitigation Architecture
| Artefact | Written by | Lives in | Fails when |
|---|---|---|---|
| Abuse case | The engineer refining the story | The ticket | It is written after implementation, as documentation |
| Acceptance criterion | The same engineer, reviewed | The ticket’s done list | It states a principle rather than a checkable outcome |
| Control baseline | The security reviewer, once per feature type | The repository, version controlled | It is a wiki page nobody opens during refinement |
| Definition of done | The team | The board | Security criteria are marked optional |
| Regression test | Whoever implements the story | The test suite | The criterion was never expressible as a test |
The single structural decision that makes this work is putting the baselines in the repository next to the code rather than in a knowledge base. A baseline that is one directory away from the file being edited gets read; one that requires opening a browser does not.
Step-by-Step Implementation
Step 1 — Write the abuse case beside the story (ASVS V1.1.3)
## Story
As a workspace member, I can invite a colleague by email so they can join the workspace.
## Abuse cases
1. As an outsider who guesses an invite URL, I join a workspace I was never invited to.
→ Gain: read access to everything in that workspace.
2. As a departing member, I invite a personal address before losing access, to keep a way back in.
→ Gain: persistent access after offboarding.
3. As a spammer with one free account, I send thousands of invitations through your mail domain.
→ Gain: delivery from a reputable sender; you gain a damaged sending reputation.
4. As a member, I enumerate which addresses already have accounts from the invite response.
→ Gain: a list of your customers' employees.
Four cases, none of which requires deep security expertise to write — they require thinking about the feature adversarially for ten minutes during refinement. That is the skill worth building, and it is teachable in a way that formal threat modelling notation is not.
Step 2 — Convert each into a testable criterion (ASVS V1.1.4)
## Acceptance criteria (security)
- [ ] Invite tokens are 128-bit random, single-use, and expire after 7 days.
- [ ] Accepting an invite requires that the authenticated address matches the invited address.
- [ ] Invites are revoked automatically when the inviting member loses workspace access.
- [ ] Invitations are rate limited to 20 per member per day and 200 per workspace per day.
- [ ] The invite response is identical whether or not the address already has an account.
Every line names something a reviewer can look for and a test can assert. Compare with “invitations should be secure”, which passes review because nobody can say it does not.
Step 3 — Give recurring feature shapes a baseline (ASVS V1.1.2)
# security/baselines/user-content.yml — applied to any feature accepting content from users
applies_to: "features that accept content from a user and display it to another user"
required_controls:
- id: BL-UC-1
control: "Content is sanitised at the render boundary for the exact context it lands in"
verify: "Payload suite renders known attack strings and asserts nothing executes"
- id: BL-UC-2
control: "Uploads are typed from content, stored under a generated name, served from the content origin"
verify: "Upload defence suite passes for this feature's endpoint"
- id: BL-UC-3
control: "Author identity is recorded and visible to moderation"
verify: "Moderation view shows author and timestamp for every item"
- id: BL-UC-4
control: "Rate limit per author, and a report path for other users"
verify: "Limit test passes; report endpoint exists and is monitored"
A baseline turns the common case into a checklist and reserves fresh analysis for the genuinely novel. It also makes review faster: “this is a user-content feature, baseline applied, plus two feature-specific cases” is a far better conversation than starting from a blank page every time.
Step 4 — Make the criteria block completion
## Definition of done
- [ ] Functional acceptance criteria pass
- [ ] Security acceptance criteria pass, each with a linked test
- [ ] Applicable control baseline referenced by identifier in the pull request
- [ ] Any accepted risk recorded with an owner and a review date
The fourth line matters as much as the second. Teams will sometimes ship with a known gap, and that is a legitimate business decision — but it should leave a dated record with a name on it, not an unspoken assumption that someone will get to it.
Edge Cases & Bypass Patterns
The abuse case written after the fact. Retrospective abuse cases describe what was built rather than what should have been considered, and they always conclude that the implementation is fine. Require them at refinement, before estimation.
Criteria that restate the control library. “Uses parameterised queries” is a coding standard, not an acceptance criterion for this story. The criterion should describe an outcome for this feature: “the report filter accepts only the declared column names, and a probe with an injected clause returns 400”.
Baselines that grow without pruning. A baseline of thirty controls is skipped wholesale. Keep each to the handful that actually apply, and move the rest into automated checks where they belong.
Stories too small to threaten anything. Applying the process to a copy change trains the team that it is ceremony. Publish the trigger list — authentication, authorization, money, personal data, file handling, outbound requests — and let everything else through with the baseline alone.
Automated Testing & CI Validation
# tests/test_invite_abuse_cases.py — one test per acceptance criterion, named after it.
def test_invite_token_is_single_use(client, invite):
assert client.post("/invites/accept", json={"token": invite.token}).status_code == 200
assert client.post("/invites/accept", json={"token": invite.token}).status_code == 400
def test_invite_bound_to_the_invited_address(client, invite, other_user):
r = client.post("/invites/accept", json={"token": invite.token}, as_user=other_user)
assert r.status_code == 403
def test_invite_revoked_when_inviter_loses_access(client, workspace, inviter, invite):
workspace.remove_member(inviter)
assert client.post("/invites/accept", json={"token": invite.token}).status_code == 400
def test_invite_response_does_not_reveal_existing_accounts(client, workspace):
existing = client.post("/invites", json={"email": "[email protected]"})
unknown = client.post("/invites", json={"email": "[email protected]"})
assert existing.status_code == unknown.status_code
assert existing.json() == unknown.json()
- name: Every security criterion has a test
run: python3 tools/check_criteria_have_tests.py --tickets .tickets/ --tests tests/
The last check is the one that keeps the practice honest over time: it fails when a ticket declares a security criterion that no test name references, which is precisely how criteria quietly become decoration.
When to Escalate Beyond the Baseline
Most stories are served by an abuse case and a baseline. Three situations warrant more: a design with no precedent in the codebase, a feature that moves a trust boundary, and anything where the worst realistic outcome would be reportable. In those cases, run a full modelling session with the technique the situation calls for — usually a goal-directed one — and treat the abuse cases as the output rather than the input. Record which route was taken and why, so the escalation decision is itself reviewable later.
Compliance Mapping
| Framework | Control | Satisfied By |
|---|---|---|
| SOC 2 | CC8.1 — change management | Security acceptance criteria in the definition of done, evidenced per pull request |
| OWASP ASVS | V1.1.2 — secure development lifecycle | Control baselines per feature type, referenced by identifier |
| OWASP ASVS | V1.1.3–V1.1.4 — threat and requirement documentation | Abuse cases and testable criteria stored with the work item |
| NIST SSDF | PW.1 — design software to meet security requirements | Requirements derived from abuse cases at refinement time |
| ISO 27001 | A.8.25 — secure development lifecycle | Documented process with tests as the evidence of application |
Common Pitfalls Checklist
Frequently Asked Questions
How is an abuse case different from a threat in a threat model?
A threat is a property of the system; an abuse case is a story about a feature. The threat says “the session identifier is not rotated at privilege change”. The abuse case says “as someone who can plant a cookie, I get the victim to sign in on an identifier I already hold, so I inherit their session”. The second sits beside a user story, gets estimated alongside the work, and yields acceptance criteria — which is why it reaches implementation far more reliably than a line in a document.
Does every story need an abuse case?
No, and insisting otherwise is how the practice dies. Most stories are covered by the baseline for their feature type, and inventing analysis for a copy change spends attention you will want elsewhere. Require abuse cases where the story touches authentication, authorization, money, personal data, file handling or outbound requests, and publish that trigger list so the rule is applied consistently rather than by mood.
Who writes them — engineers or the security team?
Engineers, with the security team providing baselines, the trigger list, and review on the hard ones. Requirements written by a security team arrive as an external checklist and are treated as one. Abuse cases written by the engineer building the feature are informed by how it actually works, and they routinely name the specific bypass that a reviewer looking from outside would never have guessed at.
How do you keep this from becoming ceremony?
Tie every abuse case to a test. If a criterion cannot be expressed as something a test or a reviewer can check, it is a principle rather than a requirement, and principles do not survive a busy sprint. The measure of success is not how many abuse cases were written; it is how many failing tests they produced before the feature shipped.
Related
- Threat Modeling Fundamentals & Methodology — the parent guide covering the full methodology
- Writing Abuse Cases for User Stories — the refinement-time practice in detail
- Security Acceptance Criteria in Agile Tickets — turning an abuse case into something testable
- Attack Trees vs STRIDE for Feature Design — choosing the analysis technique per situation
- Threat Model Documentation Patterns — where the resulting decisions are recorded