Security defects are cheaper the earlier you find them — this is an industry iron rule. If a vulnerability is found after production release, the cost is unlimited: investigation, fix, redeploy, and in some cases responding to information leakage. On the other hand, find it on a diagram at the design stage and it's just an erase with an eraser.
The systematic method for this "building security in at the design stage" is threat modeling. This article, rather than how to use abstruse tools, explains a thinking type a team can run right away, faithful to official information (the Threat Modeling Manifesto, Microsoft's STRIDE, OWASP Threat Modeling), and finally goes all the way to the implementation of managing threats "as code."
The positioning of this article: of a security engineer's core skills, this deeply explores the "Identify" in NIST CSF 2.0 terms — the ability to surface risk at the design stage. For the roadmap of the role overall, see how to become a security engineer, and for the technique of crushing the surfaced threats in implementation, secure-coding practices.
0. What threat modeling is — just answer four questions
When you hear threat modeling, you tend to imagine a dedicated tool or abstruse notation, but the essence is answering, as a team, the four questions the Threat Modeling Manifesto raises.
- What are we working on? — diagram the target system.
- What can go wrong? — surface the threats.
- What are we going to do about it? — decide on mitigations.
- Did we do a good enough job? — verify.
Running these four questions for an hour in a design-review setting is already valuable. The Manifesto says — "a valuable model over a perfect model," "people and collaboration over checklists." Threat modeling isn't a "ritual" but a collaborative activity to think one level deeper about the design.
1. STRIDE — the six categories for exhaustively enumerating threats
Listing "what can go wrong" by "feel" will surely leave gaps. So use STRIDE, systematized by Microsoft. It divides threats into six categories, each corresponding to which security property it breaks.
| STRIDE | Threat | Property broken | Example |
|---|---|---|---|
| Spoofing | Spoofing | Authentication | Log in by impersonating someone else |
| Tampering | Tampering | Integrity | Rewrite a request or stored data |
| Repudiation | Repudiation | Non-repudiation | Wriggle out with "I didn't do it" (poor logs) |
| Information Disclosure | Information disclosure | Confidentiality | Peek at someone else's data |
| Denial of Service | Denial of service | Availability | Stop the service with mass requests |
| Elevation of Privilege | Privilege escalation | Authorization | A general user gains admin privileges |
STRIDE's strength is that you can mechanically ask the six "is there spoofing for this element? tampering? …" Exhaustiveness beats person-dependent intuition.
2. Draw a data flow diagram (DFD) and trust boundaries
The best tool to share "what we're working on" is a Data Flow Diagram (DFD). More than perfect notation, what matters is seeing where data flows from and to, and where the 'premise of trust' changes.
The minimal vocabulary is just this.
- External Entity: users, external APIs (which we don't control)
- Process: API servers, functions (our code)
- Data Store: DB, cache, files
- Data Flow: data flowing between elements (arrows)
- Trust Boundary: the line where the trust level changes (most important)
┌─────────────[ 信頼境界 ]─────────────┐
(境界の外=信頼できない)
[ユーザー] ──HTTPリクエスト──▶ │ [APIサーバー] ──SQL──▶ [DB]
外部実体 │ プロセス データストア
│ │
[外部決済API] ◀──HTTPS──────── │ └──ログ──▶ [ログ基盤]
外部実体 └──────────────────────────┘
The trust boundary is the very heart of threat modeling. Data flows that cross the boundary — user input, external-API responses, calls from another microservice — are all "the moment something untrusted enters the inside." This coincides with where validation, authentication, and authorization should take effect. Once the boundary is visible on the diagram, "where to protect" becomes self-evident.
3. STRIDE × DFD — concretely enumerate threats
Apply STRIDE's six categories to each element of the DFD. For example, about the "user → API server" data flow (crossing the boundary):
| Element | STRIDE | Assumed threat | Mitigation |
|---|---|---|---|
| User authentication | Spoofing | Impersonation via a weak password / token theft | MFA, strong authentication, short-lived tokens |
| Request body | Tampering | Parameter tampering (amount, ID) | Input validation at the boundary, signing |
| Recording of operations | Repudiation | Denying with "I didn't order" | Tamper-resistant audit logs |
| Data-retrieval API | Information Disclosure | Viewing others' data via IDOR | Server-side authorization, RLS |
| Public endpoint | Denial of Service | Exhaustion via mass requests | Rate limiting, timeouts |
| Permission check | Elevation of Privilege | A general user hits the admin API | Least privilege, server-side role verification |
In this way, just filling in the "element × STRIDE" matrix makes the threat enumeration astonishingly exhaustive. You don't need to crush everything perfectly. Being "aware of it" is decisively more important than not being aware.
4. Manage the threat model "as code"
The biggest reason threat modeling becomes a formality is "writing it in a document once and letting it rot as-is." The design keeps changing, but the diagram fossilizes as a PNG somewhere on a wiki. The best way to prevent this is to manage the threat model as code, in the repository (Threat Modeling as Code).
First, hold the threat registry as typed structured data.
# threat-model.yaml — 脅威レジストリ(リポジトリで版管理する)
threats:
- id: T-001
component: "注文取得API"
stride: InformationDisclosure
description: "URLのIDを改ざんし、他人の注文を閲覧できる(IDOR)"
risk: high # high | medium | low(可能性 × 影響)
mitigation: "サーバー側で所有者条件をクエリに焼き込み、RLSで多層防御する"
mitigated: true # 緩和策が実装済みか
- id: T-002
component: "ログインAPI"
stride: Spoofing
description: "総当たりでパスワードを推測される"
risk: high
mitigation: "レート制限 + アカウントロック + 漏えいパスワードのブロックリスト照合"
mitigated: true
- id: T-003
component: "管理ダッシュボード"
stride: ElevationOfPrivilege
description: "一般ユーザーが管理APIを直接叩いて権限昇格する"
risk: high
mitigation: "全管理APIでサーバー側ロール検証を必須化する"
mitigated: false # ← まだ未対応
Next, write a verification that "if a high-risk threat has no mitigation / unimplemented, fail CI." This makes the threat model a "living contract."
// verify-threat-model.ts — 高リスクの未緩和脅威があればCIを落とす(設計と実装の乖離を防ぐ)。
import { readFileSync } from "node:fs";
import { parse } from "yaml";
import { z } from "zod";
// ① スキーマで脅威モデルを検証(型の不正・記入漏れを起動時に弾く)。
const ThreatSchema = z.object({
id: z.string().regex(/^T-\d{3}$/),
component: z.string().min(1),
stride: z.enum([
"Spoofing", "Tampering", "Repudiation",
"InformationDisclosure", "DenialOfService", "ElevationOfPrivilege",
]),
description: z.string().min(1),
risk: z.enum(["high", "medium", "low"]),
mitigation: z.string().min(1),
mitigated: z.boolean(),
});
const ModelSchema = z.object({ threats: z.array(ThreatSchema) });
function verify(path: string): number {
const model = ModelSchema.parse(parse(readFileSync(path, "utf8")));
// ② 不変条件:高リスクの脅威は、必ず緩和策が実装済みでなければならない。
const unmitigated = model.threats.filter((t) => t.risk === "high" && !t.mitigated);
if (unmitigated.length > 0) {
console.error("❌ 高リスクなのに未緩和の脅威があります:");
for (const t of unmitigated) console.error(` - ${t.id} (${t.component}): ${t.description}`);
return 1; // CIを失敗させる
}
console.log(`✅ ${model.threats.length} 件の脅威すべてに緩和策が実装済みです。`);
return 0;
}
process.exit(verify("threat-model.yaml"));
Place this script in CI and you can mechanically detect the divergence between "threats raised in design" and "implemented mitigations." The threat model changes from a "rotting document" into an "unbreakable contract" — this is a security-engineer-like design practice. The thinking on type-safe boundary validation connects to secure-coding practices.
5. Prioritizing mitigations — don't try to protect everything
Even if you raise 100 threats, resources are finite. Prioritize by risk = likelihood × impact.
| Impact: large | Impact: small | |
|---|---|---|
| Likelihood: high | Mitigate as top priority | Mitigate early |
| Likelihood: low | Mitigate or accept (record required) | Accept (record only) |
What's important is that "acceptance" is also a legitimate option. You can't zero every risk. A state where you can record "this risk is accepted, with a reason" is far healthier than unwittingly leaving it. This is also a decision that management should be responsible for, and the METI Cybersecurity Management Guidelines also position grasping risk and decision-making as a management responsibility.
6. When and how much to do it
Threat modeling isn't "do it once and you're done."
- When designing a new feature: the most cost-effective. Build 30 minutes to 1 hour of threat modeling into the design review.
- When the architecture changes: when the trust boundary moves (new external integration, a change of authentication method, etc.).
- Periodically: review the existing model, e.g., once a quarter.
"Light, frequent" is the principle. Rather than doing heavy threat modeling once a year, continuously running lightweight threat modeling per feature keeps up with change and takes root in the team as a culture.
7. Frequently asked questions (FAQ)
Q. Is threat modeling needed even for a small team / solo development? A. It is. Rather, the fewer the people, the greater the value of preventing rework at the design stage. No dedicated tool is needed. You can start by drawing a DFD on a whiteboard and asking STRIDE's six items.
Q. Should I use a dedicated tool? A. Not at first. There's the Microsoft Threat Modeling Tool and OWASP Threat Dragon, etc., but the essence is the thinking of "the four questions" and "STRIDE × DFD." Use tools once you're accustomed.
Q. What's the difference from methods other than STRIDE (PASTA, LINDDUN, etc.)? A. STRIDE has the lowest learning cost and is general-purpose. There are purpose-specific methods like LINDDUN for privacy focus and PASTA for risk-driven, but the royal road is to first get the type into your body with STRIDE.
Q. What's the difference from vulnerability assessment (penetration testing)? A. Threat modeling is the "preventive" activity of surfacing threats at the design stage, and assessment is the "discovery" activity of searching for vulnerabilities after implementation. They're two wheels. For how to do assessment, see web-app vulnerability assessment, and for the attacker's viewpoint, white-hat hacker introduction.
Q. Doesn't it slow down development? A. In the short term, design time increases. But threats crushed at the design stage are an order of magnitude cheaper than the post-release incident-response cost (response per NIST 800-61). Overall development speed actually rises.
8. Conclusion
Threat modeling is, not special talent or an expensive tool, a collaborative activity to think one level deeper about the design.
- Run the four questions. What are we working on / what can go wrong / what to do / did we do a good enough job.
- Be exhaustive with STRIDE × DFD. Draw trust boundaries and apply the six categories to each element. Beat intuition with exhaustiveness.
- Manage it as code. Hold the threat registry in YAML, verify the presence of mitigations in CI, and prevent the divergence of design and implementation.
- Prioritize, and record acceptance too. You can't protect everything. Being aware is far healthier than leaving it.
- Light, frequent. Keep running it per feature and let it take root as a culture.
Whether you can build security in at the design stage most greatly affects a product's safety. "Building fast" and "building safely" can be reconciled in the first 30 minutes of design — that's the essence of threat modeling. If you want to build threat modeling into your product's design review, or take inventory of the "vertical risks" of an existing design once, feel free to consult me.