Chapter 3: Threat Modeling and Secure Design
3.1 Why.
Welcome to Chapter 3.
I am Ravindra Bagale.
Raju had no engineering degree. He grew up in a small town in India. He still learned fast. In the Magical Code Library he held his phone over the table. The screen showed a Swiggy order. The total was 5000.
"Guru-ji," he said, "the phone is in my hand. If I change 5000 to 1 before the request leaves, the kitchen still cooks, and I pay one rupee. Is that a code bug or a story bug?"
Guru-ji took the phone and set it face down. He drew three boxes on paper. Phone. Payment API. Order store.
"The phone is a storyteller," Guru-ji said. "The order store is the book. If the API believes the storyteller, the city pays for the meal. If the API reads the book, the meal stays 5000. We do not try this on Swiggy. We draw it. This is for learning only, and only inside a system you are allowed to test."
Raju nodded. He wanted a trick. Guru-ji wanted a sentence.
The sentence is the threat model. You say what must stay true. You draw where data moves. You name what you fear. You name the fix before anyone writes the handler.
A team that skips this page finds the bug in production. Chapter 1 already showed the cost. A wrong amount on paper is an eraser mark. A wrong amount on a live payment is a loss, a fight with the customer, and a night with no sleep.
Suppose we are Paytm taking a bill payment. The phone can show any number. The server must charge the bill Paytm already stored. Suppose we are Amazon on a sale day. The cart total is not a field the browser may edit. Suppose we are BHIM UPI. The amount the user confirmed is the amount that moves. A second copy of the same request must not move the money twice.
That is why we draw before we code.
Where threat modeling came from
Threat modeling is thinking about attacks before writing code. It is cheap: a whiteboard and one hour.
STRIDE at Microsoft
Loren Kohnfelder and Praerit Garg at Microsoft described STRIDE in 1999. Spoofing, Tampering, Repudiation, Information disclosure, Denial of service and Elevation of privilege. Microsoft later built a free Threat Modeling Tool around it.
OWASP Threat Dragon
OWASP keeps Threat Dragon, a free tool where teams draw data flows and mark threats. Many teams start with it because it costs nothing.
3.2 What.
A threat model is a picture plus a few honest sentences. It is not a tool logo. It is not a permission slip to poke a real company.
Assets. An asset is what hurts if it is lost, changed, or shown. For a payment, the assets are the amount, the customer's identity, the card or UPI token, and the record that says paid. Write them down. If you cannot name the asset, you cannot protect it.
Entry points. An entry point is a door. The mobile app, a web form, a webhook from a bank, an admin screen, a support upload, and a health URL are doors. Kubernetes adds doors too: the API server, a dashboard, and a CI job that deploys. List the doors you actually have. A door you forgot is the door that stays open.
Trust boundaries. A trust boundary is a line you do not cross by hope. The phone is outside the line. The payment API is inside. The database is deeper inside. A webhook from a partner crosses the line, so the API checks a signature before it believes the body. Draw the line in ink. Anything outside the line can lie.
Data-flow on paper. You need five marks.
- An external entity is a square. The person with the phone.
- A process is a circle or a rounded box. The payment API.
- A data store is a cylinder. The orders table. It holds 5000.
- A data flow is an arrow. Label it. "order id", not "data".
- A trust boundary is a dashed line between the phone and the API.
Keep the first picture small. Four boxes beat forty. When the picture is true, add the next service.
The amount rule. The phone sends an order id. It may also send an amount. The API loads the order. The amount it charges is the amount on the order. If the body says 1 and the order says 5000, the API refuses. It does not "be helpful" and take the smaller number. It does not average them. It does not trust the client because the client is "our app". Anyone can copy a request from a phone.
Here is the rule in code. This is a teaching sample for your own lab. It is not a live Paytm or Swiggy handler.
app.post("/api/pay", async (req, res) => {
const user = req.user;
const order = await DB.findOrder(req.body.orderId);
if (!order || order.ownerId !== user.id) {
return res.status(403).json({ error: "Forbidden" });
}
if (req.body.amount !== undefined && req.body.amount !== order.amount) {
return res.status(400).json({ error: "Amount does not match the order" });
}
await charge(user, order.amount);
return res.json({ ok: true, amount: order.amount });
});
Read it slowly. The owner check is access control. The amount check is the business rule. charge never sees the phone's number. It sees order.amount.
STRIDE. STRIDE is six kinds of threat. Use it on each box and each arrow. One sentence of fear. One sentence of fix. No procedure.
Spoofing. Someone wears another person's login. The API thinks the wrong human is paying. Fix: the server proves identity. A user id in the JSON body is not proof. The session or token is checked, and the token is tied to the user.
Tampering. Data changes on the way or in the store. The picture is 5000 becoming 1. Fix: the server re-reads the order. A signature such as HMAC can show that a partner's body was not edited. This chapter does not show how to build or break that signature.
Repudiation. Someone says "I did not pay" or "I did not refund", and you have no record. Fix: log who asked, which order, which amount the server accepted, and the time. Do not log the full card or the password.
Information disclosure. A secret is shown. Another customer's address, a token, a stack trace. Fix: return only this user's fields. Mask the rest. Keep dumps out of tickets.
Denial of service. A flood, or one very heavy request, keeps real users out. Fix: limit how much one caller can send, and shed load. This page will not describe how to make a flood.
Elevation of privilege. A normal user becomes an admin, or one restaurant reads another's orders. Fix: the server decides the role. A flag on the phone that says admin: true is a joke, not a role.
Say them once. Spoofing is a mask. Tampering is a changed number. Repudiation is a missing diary. Information disclosure is a secret in the open. Denial of service is a crowd at the door. Elevation of privilege is a borrowed crown.
Attack trees. An attack tree starts at a bad goal and asks "what would have to be true?" You stop at the control. You do not write the steps of the crime.
Goal: the meal is paid as 1 instead of 5000.
- Branch: the phone's amount is the amount charged. Control: charge
order.amountonly. - Branch: the order row itself is edited to 1 by someone who should not write it. Control: the app role cannot update amount. A separate, reviewed job can.
- Branch: a webhook says "paid" and nobody checks who sent it. Control: verify the partner signature, then mark that order paid once.
If a branch turns into commands, you left the lesson. Come back to the control.
Misuse and abuse. A misuse case is a real user doing a real job in a bad way. "I open my friend's order id because the URL looks the same." An abuse case is someone using the feature as a weapon. "I place hundreds of cash orders so the Swiggy restaurant cannot take honest ones." Write both in one sentence each. Then write the control. For the friend: the server checks ownerId. For the flood of orders: a rate limit and a rule that unpaid orders expire.
PASTA. PASTA is seven thinking steps. It is a walk, not an attack plan.
- Define objectives. The amount that moves is the amount on the order. Card data stays private. One payment id pays once.
- Define the technical scope. Phone, order API, payment API, and the order database are in the picture. The card network is a neighbor. You do not pretend to own it.
- Decompose the app. Draw the data-flow. Mark trust boundaries, assets, and entry points.
- Analyze threats. Put STRIDE on each arrow. One sentence each.
- Analyze weaknesses. Example: the handler trusts
req.body.amount. Name it. Do not turn it into instructions. - Model the attack in one or two sentences. "A caller who can speak HTTP can send a different amount." Stop. No payload. No tool command.
- Rate the harm and name the countermeasure. Harm: lost money and a false "paid" flag. Countermeasure: server-side amount, owner check, one-time payment id, and a log line.
Guru-ji sends Raju back to step 1 whenever step 6 gets exciting. "Move money correctly" is an objective. "See if I can trick a gateway" is not an objective of this chapter.
Threat libraries. A library is a shelf of known names so you do not rely on memory alone. OWASP's lists, and MITRE ATT&CK as a catalog of adversary behavior, are shelves. You use them to ask "did we forget this class of threat?" You do not copy a procedure out of them into a lab aimed at someone else. In this chapter the library is a checklist of words.
Microservices and APIs. Suppose an order service and a payment service talk inside Amazon's shape of a shop, or inside Swiggy's shape of a delivery. If the network between them is flat, anyone who reaches that network can say "this order is paid". Fix: each service has an identity. The payment service accepts "paid" only from the payment worker, and it still loads the amount from the order store. An API gateway can check the token at the edge. The service still checks the object. A gateway is not a substitute for ownerId.
Kubernetes and cloud, on paper. A cluster is a city of pods. Draw these threats without touching a live cluster you do not own.
- A pod with a powerful service account can read secrets it does not need. Fix: a small account per workload.
- Two tenants on one cluster can see each other if the network is open. Fix: separate namespaces and a network policy that denies by default.
- A cloud role on the app can list every bucket. Fix: one role, one bucket, no list-the-world permission.
- A public load balancer and a private database must not share one flat trust. The database does not sit on the public side of the line.
Authentication and authorization design. Authentication answers "who are you?" Authorization answers "may you do this to this object?" A design review asks both. Login can be perfect and still be broken access control if any logged-in user can open any order id. Review the token too. Who issues it? When does it die? Does the API check the audience, so a token for the profile API is useless at the payment API? Where is the signing key? Not in the phone.
Secrets and trust. The payment key lives in a secrets store the server reads. It does not live in the app binary, in the picture's margin, or in a chat. A webhook trust is a shared signing secret or a public key you pinned. If that secret is in Git, the trust is already public. Chapter 6 is about that mistake. Here, mark the secret as an asset on the diagram.
Multi-tenancy. Swiggy has many restaurants. Zomato has many restaurants. Infosys on a client project may host more than one client. Tenant A must not read tenant B. The tenant id comes from the server session, not from a query field alone. If the client sends restaurantId, the server checks that this user may act for that restaurant. Isolation is a design choice. A comment that says "be careful" is not isolation.
Business logic and fraud. Not every bug is a crash. Some bugs follow the code and still steal. A coupon used twice. A refund larger than the payment. A tip that the phone sets after the restaurant already cooked. Write the rule in a sentence the server can enforce. "Refund amount is at most the captured amount." "A coupon id is consumed once." "The delivery fee comes from the distance table, not from the app." Fraud controls sit beside these rules: velocity limits, a hold on a new device, and a human review when the amount jumps. Do not build a crime script. Build the rule.
Rate limits, anti-automation, replay. A payment request carries a one-time id, sometimes called an idempotency key. The server stores it. The same id again returns the first result and does not charge twice. That is replay protection. A rate limit caps how many pays one user or one IP range can attempt in a minute. Anti-automation means a bot cannot pretend to be a thousand customers without cost. Captchas are one tool. Device signals are another. The lesson is the limit, not a way around it.
Errors. When the charge fails, the order stays unpaid. The client sees a short message. It does not see a stack trace, a SQL line, or a cloud key. Fail closed. A timeout is not a success. If the server is unsure, it does not mark paid. It reconciles. Guru-ji calls this "the door shuts when the hinge breaks."
Keeping the model alive. A threat model rots when the architecture moves and the paper does not. Add a box, update the sheet in the same change. Retire a box, cross it out. Once a quarter, read the sheet aloud. If nobody can find it, you do not have a model. You have a memory.
Evidence on the pull request. Automation here is modest and real. The pull request template asks three questions. What new entry point did you add? What asset does it touch? Which STRIDE line did you update? A reviewer can see the answer. A bot can check that the threat-model file changed when the routes file changed. That is evidence. It is not a scan of the whole internet.
A small e-commerce picture for practice. Draw four services for a shop in the shape of Amazon or Swiggy. The app. The order API. The payment API. The catalog. Put the trust boundary at the app. Put 5000 on the order. Put a coupon on the catalog. Then write six STRIDE lines for the payment arrow only. Then write three tests you would run in your own lab: the owner check, the amount check, and the replay check. Expected results: 403, 400, and a single charge.
Ravindra Bagale's Tip
Students skip the picture and collect attack recipes. That habit is unsafe, and it does not teach judgment. If you cannot say the threat in one sentence and the fix in one sentence, you are not done.
Practice task
On paper, draw the phone, the payment API, and the order store for a 5000 order. Write one tampering sentence and one fix sentence. Then write one replay sentence and one fix sentence. Do not write a request a person could send to a company you do not own.
3.3 How.
- Write the system name at the top of a blank page, and write "learning only, own lab or a system you may test."
- Write the objective in one line: the charged amount equals the stored order amount, and one payment id pays once.
- List the assets: amount, identity, payment instrument, and the paid flag.
- List the entry points you actually ship: app, webhook, admin, and health check.
- Draw the external entity, the process, the data store, and label every arrow.
- Draw the trust boundary between the phone and the API.
- Mark the amount on the store as 5000, not on the phone.
- For the payment arrow, write one STRIDE sentence and one fix for each of the six words.
- Draw an attack tree with the bad goal "pay 1 instead of 5000" and stop each branch at a control.
- Write one misuse case and one abuse case, each in a single sentence, each with a control.
- Walk PASTA's seven steps in order and do not let step 6 become a procedure.
- Name the neighbor you do not own, such as the card network or BHIM UPI, and leave it outside the boxes you pretend to control.
- For each microservice hop, say which identity is allowed to call, and say no if the answer is "anyone on the network."
- For a Kubernetes shape, write the service account name and the one secret it may read.
- For a cloud shape, write the one bucket or table the app role may read.
- Separate authentication from authorization in two sentences.
- Put the payment secret in a secrets store on the drawing, not in the app box.
- Add the tenant check: the server, not the query string, decides the restaurant or the client.
- Write the business rules the server enforces: refund ceiling, one coupon use, server-side fee.
- Add a one-time payment id and a rate limit to the picture.
- Write the error behavior: unpaid on failure, no stack trace, no success on timeout.
- Save the sheet next to the code and update it in the same pull request that adds a route.
- Add three lines to the pull request template: new entry point, asset touched, STRIDE line updated.
- In your own lab, implement the owner check and the amount check from Part 2, and refuse a body amount that is not the order amount.
- In your own lab, send the same payment id twice and confirm the second call does not charge again.
- Stop if the system is not yours and nobody has told you in writing that you may test it.
A one-hour session
- Draw the system: users, app, database, outside services.
- Draw a dashed line at every trust boundary (internet to app, app to database).
- For each arrow crossing a line, ask the six STRIDE questions.
- Write each real threat as a ticket with an owner.
- Repeat when the design changes.
| STRIDE letter | Example threat | Typical control |
|---|---|---|
| Spoofing | Stolen password logs in | MFA, short sessions |
| Tampering | Order amount changed in the browser | Server-side checks |
| Repudiation | User denies a payment | Signed audit logs |
| Information disclosure | Public S3 bucket | Block Public Access |
| Denial of service | Login flood | Rate limits, WAF |
| Elevation of privilege | User calls admin API | Role checks on server |
3.4 A real public incident.
Name. Capital One data security incident.
Date. Unauthorized access on 22 and 23 March 2019. Capital One determined that on 19 July 2019. The company announced it on 29 July 2019.
Source. Capital One's news release of 29 July 2019, also filed with the U.S. Securities and Exchange Commission. Later public documents, including a U.S. Department of Justice complaint and reporting by Krebs on Security in August 2019, described the shape of the failure. This chapter uses those sources for what happened. It does not repeat steps.
What happened. Capital One said an outside individual obtained personal information of people who had applied for its credit cards and of Capital One credit card customers. The company said the person used a configuration vulnerability, and that this kind of weakness is not unique to the cloud. Its announcement described the event as affecting about 100 million people in the United States and about 6 million in Canada. Public reporting described a misconfigured web application firewall and permissions that were wider than the job. A public-facing piece could be made to reach a cloud metadata service and then a role that could read too much customer data. That is a missing trust boundary. The picture was never drawn, or it was drawn and not enforced.
How to stop.
- Fix the configuration that let an outside request reach an internal credential path.
- Revoke the credentials that were exposed.
- Stop further reads of the customer data stores.
- Work with law enforcement.
- Tell the people whose information was taken.
How to prevent it again.
- Draw the trust boundary before the firewall or the API goes live.
- Do not let a public component fetch an address that a caller chooses, especially an address only the server should see.
- Give that component a role that can do one job, not list every store.
- Alert when a large copy of customer data starts.
- When the architecture changes, update the threat model in the same change.
- Practice only in a lab you own. Do not look for this condition on a bank, on Amazon, or on any cloud account that is not yours.
This is for learning only, and only inside a system you are allowed to test.
3.5 A simple picture.
The phone is outside. The API is inside. The order store holds 5000. A dashed note shows the phone saying 1. That number never becomes the charge.
3.6 A short animation.
Watch the phone offer 1. Watch the order answer 5000. The stamp is the server. It does not negotiate.
3.7 Conclusion.
Raju looked at the paper, not at the phone. "The storyteller can say 1," he said. "The book says 5000. The API reads the book."
Guru-ji closed the notebook. "That is secure design. STRIDE names the fear. PASTA orders the walk. The picture keeps the team honest. In Chapter 4 we will name the ten risks the OWASP Top 10:2025 puts on a web application, and we will keep the defensive code beside each name."
- A threat model is a picture, an asset list, and a fix you can point at.
- The payment amount is checked on the server against the stored order.
- STRIDE is six words. PASTA is seven steps. An attack tree stops at the control.
- Multi-tenant, cloud, and Kubernetes pictures use the same lines: identity, least privilege, and a boundary.
- Capital One, announced 29 July 2019, is the lesson about a boundary that was not enforced.
- Paper first. Your own lab only. No one else's payment API.
END OF CHAPTER 3 MASTER CLASS
Threat modeling found the cheapest fixes in this chapter. Keep the diagram next to the code and update it with each big change.
| Do | Avoid |
|---|---|
| Model early, in design | Modeling only after release |
| Mark trust boundaries | Assuming the internal network is safe |
| Turn threats into tickets | Keeping threats in a slide nobody reads |