Daniel Cohen·
Lightweight threat model I run on every new endpoint spec before implementation. STRIDE walk plus abuse cases — it reliably finds the boring authz gap everyone forgot about.
A 30-minute threat model for a single API endpoint: data-flow walk, STRIDE pass, abuse cases from the attacker's economics, and authz checks at every trust boundary.
Endpoint Threat Model Express
You are an application security engineer threat-modeling ONE new API endpoint before implementation. Depth over breadth: a thorough model of this endpoint beats a shallow model of the system. Assume the attacker has read our docs, owns a valid low-privilege account, and is patient.
Endpoint spec (method, path, params, request/response shapes, who may call it):
{{endpoint_spec}}
Auth model of the service (how identity and permissions are established): {{auth_model}}
What this endpoint touches (tables, services, queues, files): {{data_access}}
Work through:
1. **Data flow walk.** Trace the request from edge to storage and back: every trust boundary crossed, every place the request's claims are (or should be) re-checked. Name where identity is established versus where it is merely ASSUMED to have been established upstream — that gap is where authz bugs live.
2. **STRIDE pass, ruthlessly practical.** For each letter: the single most plausible threat for THIS endpoint, not a textbook list. Skip letters honestly if not applicable and say why.
3. **Authorization specifics.** The three classics, checked explicitly: (a) IDOR — every ID in path/body/query: is ownership verified against the session, not just existence? (b) function-level — can a low-privilege role reach this at all, and is that intended? (c) mass assignment — which request fields does the handler bind, and what happens if the client sends fields from the response shape (id, ownerId, role, createdAt)?
4. **Abuse cases.** Two scenarios where a VALID user uses the endpoint as designed but against our interests: enumeration economics (what can be harvested at 10 req/s for a week?), and state manipulation (race conditions on the check-then-act sequences you identified in step 1).
5. **Output.** A findings table: threat, likelihood (attacker effort), impact, and the specific control — named as an implementation instruction ('verify prompt.ownerId equals session.userId in the service layer, return 404 not 403'), not a platitude ('validate input'). End with the top 3 controls that must exist before this ships.
Ergebnisse
## Threat model: `POST /api/workspaces/:wsId/invites`
**1. Data flow walk.** Edge (rate limit, session cookie) → router (wsId from path) → invite service → DB (invites, memberships tables) → email queue. Identity established at edge middleware; **the invite service ASSUMES workspace membership was checked upstream — but the router only validates the session exists, not membership of :wsId.** That is the gap: nothing between edge and DB verifies the caller belongs to the workspace they are inviting into. Classic assumed-upstream authz hole.
**2. STRIDE (plausible-only):**
- **S**poofing: skipped — session mechanism is out of scope for this endpoint and already modeled.
- **T**ampering: `role` field in the invite body — see mass assignment below.
- **R**epudiation: no audit record of who created an invite; matters here because invites grant access.
- **I**nfo disclosure: the error response differs for 'already a member' vs 'user not found' — that difference is an email oracle for the entire user table.
- **D**oS: each invite triggers a synchronous email send; the queue is async but the template render is not.
- **E**levation: the core finding — invite with `role: "admin"` from a member-level caller.
**3. Authorization specifics.**
- **(a) IDOR:** `:wsId` — existence checked, ownership NOT (see walk). Attacker with any valid account can invite themselves into arbitrary workspace IDs. Sequential integer IDs make this a 5-line script.
- **(b) Function-level:** spec says 'members may invite' but the org's own docs say only admins manage roles — the spec conflicts with itself when the invite carries a role. Needs a product decision, recorded.
- **(c) Mass assignment:** request DTO mirrors the invite entity. Sending `{"status": "accepted"}` in the create body — does the handler bind it? If yes, invites skip acceptance entirely. Bind an allowlist: email, role. Nothing else.
**4. Abuse cases.**
- **Enumeration:** at 10 req/s against the email-oracle error difference, a week harvests ~6M address checks — validated membership intel for phishing. Uniform response + rate limit per workspace fixes both.
- **Race:** two concurrent invites for the same email pass the 'not already invited' check together → duplicate invites, and with the mass-assignment bug, potentially duplicate memberships. Unique constraint on (wsId, email) is the fix, not application-level checking.
**5. Ship blockers:**
1. Membership check for `:wsId` in the service layer — return 404 (not 403 — don't confirm the workspace exists).
2. DTO allowlist binding: `email`, `role` only; role capped at caller's own role.
3. Uniform invite response regardless of target account existence, plus per-workspace rate limit (20/hour).
Modell: Claude Opus 4.6
26 Likes17 SavesScore: 19
1 Kommentar
Luca Brunner·
"Return 404 not 403" as an explicit instruction is the level of specificity security reviews should always have. 403 on a guessed ID is a confirmation service for attackers.
