September 12, 2026
SECURITY BEGINS WITH “WHAT COULD GO WRONG?”

Class 47 gave you locks for identity and permission: who someone is, what they may do, and where that decision is enforced. Now we step back and ask the question that comes before any lock is chosen. A feature can work perfectly in your browser and still be wide open — because working and secure are two different claims.
Working is not secure
Here is the sentence that reframes everything in this class:
When you click Save on your own watchlist and see the confirmation, you have proven one path: an honest, signed-in user, doing the expected action, with valid input, right now. Security is everything outside that path: malformed requests, missing identities, borrowed record IDs, leaked secrets, a bot sending ten thousand requests at 3 a.m.
Think of it the way a building inspector thinks. The fact that the front door opens with your key says nothing about whether the windows latch or what happens when the power fails. AI builders feel this gap sharply, because an AI can generate a polished, working feature in minutes — complete with a confident "all done!" — without ever considering the hostile paths. Your job is to supply that consideration, in writing, before the AI writes code.
The tool for it is the threat model: a practical statement of what must be protected, from whom, and how failure could happen. A working document, one page if possible, that changes what gets built.
The nine boxes of a lightweight threat model
Security professionals have heavyweight versions of this. You need the lightweight version you will actually use. It has nine boxes:
1. Assets — what must be protected? Data, money, access, reputation. 2. Actors — who might cause harm, deliberately or accidentally? Anonymous visitors, other users, bots, a malfunctioning agent, a former teammate with a stale credential. 3. Entry points — where can the outside world touch the system? Every form, URL parameter, API route, upload, webhook, redirect, and admin control. 4. Trust boundaries — where does the "trusted inside" end? Browser versus server, your code versus a provider, one user's records versus another's. 5. Likely failures — what could concretely go wrong at each entry point? Invalid input, missing identity, wrong permission, leaked secret, runaway cost. 6. Impact — if it fails, how bad is it? Private data exposed, money spent, accounts taken over, sources corrupted, service down. 7. Existing controls — what already stops it? Server-side validation, ownership checks, rate limits, secret storage, database policies, logs and alerts. 8. Evidence — how do you know the control works? A named test, a log event, a review note — not an AI's assurance. 9. Owner — who is responsible for this risk, and when is it reviewed next?
If a box is blank, that is a finding, not a shrug. "Owner: nobody" means the risk is unmanaged.
Research Desk assets: know what you are guarding
Apply this to the running application, Research Desk: a person searches a public company, reads a source-linked brief, saves private companies to a watchlist, and can request a refreshed brief. Modest — but the assets are real:
- Private watchlists — each user's saved companies are private. Leaking User B's list to User A is a confidentiality failure.
- Accounts and sessions — sign-in state that, if stolen or forged, lets someone act as another person.
- Provider API keys — the secrets that buy market and filing data. Leaked keys mean someone else spends your budget.
- Paid data entitlement and spending controls — every refresh costs money. An unbounded refresh endpoint is an open wallet.
- Source integrity — briefs must cite real sources. Fabricated sources destroy the product's reason to exist.
- Operational admin access — tooling that triggers refreshes or inspects users. A privileged back door.
- Refresh and rate controls — the scheduled worker and quotas that keep costs bounded.
Notice how ordinary this list is. No state secrets. Still, each asset maps to a failure your users would feel immediately.
The one-line permission bug beats the clever attack
Beginners imagine security failures as sophisticated intrusions. Experienced builders know the highest-risk bug is often one line:
// The route loads the record... and forgets to ask whose it is.
const item = await db.watchlist.findById(id);
// missing: if (item.owner_id !== user.id) deny();
That single missing check — the exact failure Class 47 taught you to prevent — exposes every private watchlist to anyone who guesses a record ID. No cryptography was broken. One line was never written.
This is where Part X's risk tiers return. Tie every threat to a tier so the response is proportional:
| Tier | Meaning | Example | Response |
|------|---------|---------|----------|
| Low | Inconvenience, no data or money at stake | Misleading empty-state text on a public page | Fix in normal flow |
| Medium | One user's data or experience harmed | User sees a stale brief after a failed refresh | Task card + test + review |
| High | Many users, private data, money, or accounts at stake | Any-user-can-read-any-watchlist; unbounded paid refresh | Stop, shrink scope, senior/human review, proof before ship |
The one-line permission bug is High tier. A typo in a loading message is Low. The threat model exists so the High-tier item changes the task card — tighter scope, named tests, restricted files, a required reviewer — before the AI writes a line.
Three abuse stories for Research Desk
Alongside every user story ("as a signed-in user, I can save a company so I can track it"), write abuse stories: concrete misuse attempts, each with an expected denial. Here are three to start:
Abuse story 1: the signed-out save. A signed-out visitor crafts a POST /api/watchlist request by hand — no browser UI, just a raw request. Expected: the server rejects it as unauthenticated, logs the denied attempt, and reveals nothing about other users' data.
Abuse story 2: the borrowed record ID. User A, signed in legitimately, requests User B's watchlist item by swapping the ID. Expected: the server compares owner_id against the caller, denies with a non-revealing "not found or not yours" response, and logs the attempt. It needs a two-identity test: owner allowed, other user denied, anonymous denied.
Abuse story 3: the refresh flood. A bot — or a buggy retry loop — sends 10,000 refresh requests for expensive briefs in an hour. Expected: rate limits, per-user quotas, and budget caps engage; legitimate users get a clear "try again later" message; spending stops at a ceiling; an alert fires. Without this story, nobody specifies the limit, and the AI builds an endpoint with no ceiling.
Each abuse story follows the same shape: *actor → action → entry point → expected denial → evidence*. That last word is doing heavy lifting, which brings us to practice.
Exercise: write THREAT-MODEL.md for one feature
Pick one feature — "save to watchlist" is ideal. Create THREAT-MODEL.md with:
- The nine boxes filled in (assets through owner), even if briefly.
- Five abuse stories in actor/action/entry-point/expected-denial/evidence form. Start with the three above and add two of your own — for example, a giant malformed ticker input, and a stale session replaying a save after logout.
- A risk tier per story, with the High-tier items flagged.
- For each story: the control and the test or log event that proves it.
Prompt your AI like this: "Given this feature spec and the nine-box template, draft five abuse stories with risk tiers, controls, and evidence. Do not write implementation code yet." Then review: every High-tier story must change the implementation task card before coding starts. The OWASP Top 10 is a useful cross-check here: skim it and ask which of the ten categories each of your stories belongs to.
Finish line: a THREAT-MODEL.md for one feature, with five abuse stories, risk tiers, controls, evidence, and an owner — linked from the feature's task card.
Verify: can you point to one task-card requirement that exists only because of the threat model? If not, the model was decoration.
Common failure: writing "hackers might attack us" instead of actor-plus-entry-point stories. Vague dread produces no tests.
Check your understanding
1. Why does "it works in my browser" prove almost nothing about security? 2. Name the nine boxes of the lightweight threat model. 3. Why can a one-line permission mistake outrank a sophisticated attack in risk?
Next, Lesson 48.2 takes the two most abused entry points — inputs and secrets — and gives you validation layers plus secret-handling rules that belong in every task card.
ARTICLE DISCUSSION
JOIN THE
CONVERSATION.
Got a question, a take, or a better way to do this? Log in and leave a comment.
