Lesson 1.c / Module 1

The vulnerability classes that matter

The OWASP Top 10, the OWASP API Security Top 10, and the CWE Top 25 are maps of the same territory. Underneath them sit four root causes, and fixing a root cause removes a whole class.

Reading
20 min
Diagrams
8
Programs
1
Difficulty
1 of 5
Published
On this page

The idea in one minute#

Application security has three widely used lists of what goes wrong. The OWASP Top 10 ranks risk categories for web applications. The OWASP API Security Top 10 does the same for APIs. The CWE Top 25 ranks specific weakness types by how often and how severely they appear in published vulnerabilities. Each is useful, and each is easy to misuse as a syllabus or a compliance target.

Read them instead as maps. Most entries on all three lists come from four root causes: the code never asked whether the caller may do this (missing authorization), the code let data be read as code or as a command, path, or address (interpreting input), the code believed something the client said (trusting the client), or the system shipped in an open state (unsafe defaults). The consequence to act on: when you find a bug, name the root cause, then fix it where every instance goes through, so the next instance cannot be written.

A picture#

Many list entries, four root causes, four systemic fixes
flowchart LR
  L1["<b>A01, API1, API3, API5</b><br/><small>CWE-862, CWE-639</small>"] --> R1["<b>Missing authorization</b>"] --> F1["<b>One policy check</b><br/><small>every object, deny by default</small>"]
  L2["<b>A03, A10, API7</b><br/><small>CWE-89, 79, 22, 918, 502</small>"] --> R2["<b>Interpreting input</b>"] --> F2["<b>Data stays data</b><br/><small>parameterize, encode, allow-list</small>"]
  L3["<b>A07, API2, API6</b><br/><small>CWE-287, CWE-352</small>"] --> R3["<b>Trusting the client</b>"] --> F3["<b>Server derives state</b><br/><small>identity, price, intent</small>"]
  L4["<b>A05, API8</b><br/><small>CWE-611, CWE-306</small>"] --> R4["<b>Unsafe defaults</b>"] --> F4["<b>Secure by default</b><br/><small>config as code, checked in CI</small>"]
  class L1,L2,L3,L4 neutral
  class R1,R2,R3,R4 warn
  class F1,F2,F3,F4 good
classDef neutral fill:#faf9f5,stroke:#5f5e55,color:#141413,stroke-width:1.5px
classDef compute fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef memory fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef queue fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef io fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef default fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef warn fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:2px,stroke-dasharray:5 3
classDef good fill:#e2efd8,stroke:#2b6a24,color:#141413,stroke-width:2px

The mapping is not one-to-one, and the lesson says where it leaks. It is close enough to organize a review, a backlog, and the rest of this course.

How it really works#

Three lists, three purposes#

ListMaintained byWhat an entry isBuilt fromBest used for
OWASP Top 10OWASP FoundationA broad risk category, mapped to many CWEsContributed testing data plus a community surveyAwareness, shared vocabulary, a first review checklist
OWASP API Security Top 10OWASP API Security ProjectA risk category specific to APIsPublic data on API security incidents and practitioner inputReviewing an API design and its authorization model
CWE Top 25MITRE, with CISAOne weakness type, a single CWE IDCVE records mapped to CWEs, scored by frequency and severityPrioritizing weakness classes; tagging findings precisely

CWE itself (Common Weakness Enumeration) is not a ranking. It is a catalog of weakness types, organized from abstract classes down to specific variants, each with a stable numeric ID. CWE-89 means SQL injection to every scanner, bug tracker, and advisory that uses it. That precision is why you should tag findings with CWE IDs even if your team thinks in OWASP categories.

OWASP describes its Top 10 as an awareness document. For a checkable standard of what an application should do, OWASP points to its Application Security Verification Standard (ASVS). Treating the Top 10 as the full scope of a security review leaves out everything that did not make the cut.

The editions this lesson uses, in order. Check which one a document cites before comparing category numbers.

The lists are versioned: category numbers mean an edition
timeline
  title Editions referenced in this lesson
  2019 : OWASP API Security Top 10, first edition : injection is a category
  2021 : OWASP Top 10 : XSS joins injection, XXE joins misconfiguration : three new categories
  2023 : OWASP API Security Top 10 : injection dropped, three authorization entries
  2024 : CWE Top 25 : CWE-79 ranks first
  2025 : OWASP Top 10 update in progress

The OWASP Top 10 (2021)#

This lesson uses the 2021 edition, with a 2025 update in progress as of late 2025. Check the project page for the current status before citing category numbers in your own work.

IDCategoryMostly root cause
A01Broken Access ControlMissing authorization
A02Cryptographic FailuresOutside the four: weak or missing cryptography
A03Injection (cross-site scripting included)Interpreting input
A04Insecure DesignAny of them, decided before code was written
A05Security Misconfiguration (XML external entities included)Unsafe defaults
A06Vulnerable and Outdated ComponentsOutside the four: what you depend on
A07Identification and Authentication FailuresTrusting the client, weak credential handling
A08Software and Data Integrity FailuresInterpreting input (deserialization), unverified updates
A09Security Logging and Monitoring FailuresOutside the four: what you can see
A10Server-Side Request ForgeryInterpreting input

Three changes in 2021 are worth knowing because they show how the categories are built. Cross-site scripting stopped being its own entry and joined injection: both are data interpreted as code, in the browser or in the database. XML external entities joined security misconfiguration, because the fix is usually a parser setting. And three categories were new: Insecure Design, Software and Data Integrity Failures, and Server-Side Request Forgery, the last added on the strength of the community survey rather than the testing data.

Broken access control at A01 is the headline. It is also the category that automated scanners find least well, because a scanner does not know that invoice 1002 belongs to Bob.

The OWASP API Security Top 10 (2023)#

IDCategoryMostly root cause
API1Broken Object Level AuthorizationMissing authorization
API2Broken AuthenticationTrusting the client, weak credential handling
API3Broken Object Property Level AuthorizationMissing authorization (which fields), trusting the client (mass assignment)
API4Unrestricted Resource ConsumptionUnsafe defaults: no limits
API5Broken Function Level AuthorizationMissing authorization
API6Unrestricted Access to Sensitive Business FlowsTrusting the client to behave like a human customer
API7Server Side Request ForgeryInterpreting input
API8Security MisconfigurationUnsafe defaults
API9Improper Inventory ManagementOutside the four: routes and versions nobody tracks
API10Unsafe Consumption of APIsTrusting the client, where the “client” is a third-party API you call

Two things stand out. Three of the ten are authorization (API1, API3, API5). APIs expose object IDs and field names directly, and every one is a key the caller can change. And injection is not a category in the 2023 list, although the 2019 edition had one. Injection did not go away; the project chose to focus on risks that are specific to APIs. API10 is the subtle one: data your service receives from a partner API is as untrusted as data from a browser, even though your code initiated the call.

One API call carries all three keys. Each is checked by a different entry, and each check can be the one that is missing.

Function, object, and property: three keys in one request, three authorization entries
flowchart LR
  C(["Caller"])
  F["<b>PATCH /api/orders/{id}</b><br/><small>the function</small>"]
  O["<b>id = 1002</b><br/><small>the object</small>"]
  P["<b>body field status</b><br/><small>the property</small>"]
  A5["<b>API5</b><br/><small>may this caller call it?</small>"]
  A1["<b>API1</b><br/><small>may this caller touch 1002?</small>"]
  A3["<b>API3</b><br/><small>may this caller set status?</small>"]
  C --> F
  C --> O
  C --> P
  F --> A5
  O --> A1
  P --> A3
  class F,O,P warn
  class A5,A1,A3 good
classDef neutral fill:#faf9f5,stroke:#5f5e55,color:#141413,stroke-width:1.5px
classDef compute fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef memory fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef queue fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef io fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef default fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef warn fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:2px,stroke-dasharray:5 3
classDef good fill:#e2efd8,stroke:#2b6a24,color:#141413,stroke-width:2px

CWE and the Top 25#

The CWE Top 25 is computed from published CVE records that carry a CWE mapping, ranked by a score that combines how often a weakness appears and how severe the resulting vulnerabilities are. MITRE publishes it annually. In the 2024 edition, cross-site scripting (CWE-79) ranked first, and out-of-bounds write (CWE-787), SQL injection (CWE-89), cross-site request forgery (CWE-352), and path traversal (CWE-22) filled the rest of the top five.

Two properties of the list matter when you use it:

  • It covers all software. Memory-safety weaknesses such as out-of-bounds write and use after free rank high because of code in C and C++. A service in Python, Java, or Go mostly inherits them only through native dependencies.
  • It reflects what gets reported. A weakness that is common but hard to detect, or rarely assigned a CVE (business-logic flaws in a private SaaS application, for example), is underrepresented.

The CWE IDs that recur through this course:

CWENameRoot causeCourse lesson
CWE-862Missing AuthorizationMissing authorizationAuthorization
CWE-639Authorization Bypass Through User-Controlled KeyMissing authorizationBroken object-level authorization
CWE-89SQL InjectionInterpreting inputInjection and parameterization
CWE-79Cross-site ScriptingInterpreting inputXSS and output encoding
CWE-918Server-Side Request ForgeryInterpreting inputServer-side request forgery
CWE-22Path TraversalInterpreting inputFiles and paths
CWE-502Deserialization of Untrusted DataInterpreting inputParsers and deserialization
CWE-287Improper AuthenticationTrusting the clientAuthentication
CWE-352Cross-Site Request ForgeryTrusting the clientSessions and cookies

Learn the distinction between CWE-862, Missing Authorization (there is no check) and CWE-863, Incorrect Authorization (there is a check, and it is wrong). They fail differently and are found differently: the first by listing routes, the second by reading the policy.

The two weaknesses split at one question, and the answer tells you how to look for them.

CWE-862 or CWE-863: is there a check, and does it test the right thing?
flowchart TB
  R["<b>Route reaches an object or function</b>"]
  Q1@{ shape: diam, label: "Is there an authorization check?" }
  Q2@{ shape: diam, label: "Does it test server-owned facts?" }
  M["<b>CWE-862 Missing Authorization</b><br/><small>found by listing routes</small>"]
  I["<b>CWE-863 Incorrect Authorization</b><br/><small>found by reading the policy</small>"]
  OK["<b>Check holds</b>"]
  R --> Q1
  Q1 -->|no| M
  Q1 -->|yes| Q2
  Q2 -->|"no, e.g. tenant from the body"| I
  Q2 -->|yes| OK
  class M,I warn
  class OK good
classDef neutral fill:#faf9f5,stroke:#5f5e55,color:#141413,stroke-width:1.5px
classDef compute fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef memory fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef queue fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef io fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef default fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef warn fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:2px,stroke-dasharray:5 3
classDef good fill:#e2efd8,stroke:#2b6a24,color:#141413,stroke-width:2px

The four root causes#

Root causeThe belief the code should not have heldSystemic fixWhere in this course
Missing authorization“If they can name it, they may use it.”One policy decision point called on every function and every object, deny by defaultAuthorization and centralizing policy
Interpreting input“This string is just data.”Keep data out of code: parameterized queries, contextual output encoding, allow-listed destinations, parsers that cannot executeUntrusted input
Trusting the client“The client says so.”Derive identity, prices, state, and intent on the serverThe request is a trust boundary and authentication
Unsafe defaults“The defaults are probably fine.”Secure configuration as code, reviewed and tested, with insecure options requiring an explicit opt-inSecure defaults

Interpreting input deserves a sharper statement because it covers so much. A vulnerability of this class exists whenever attacker data reaches an interpreter in a position where it can change the structure of what is interpreted rather than just fill a slot: SQL, HTML and JavaScript, shell commands, file paths, URLs, XML entities, template languages, serialized object graphs. The fix is always the same in shape: an API that keeps the structure fixed and passes data separately (bound parameters, auto-escaping templates, argument arrays instead of shell strings, an allow-list of destinations, a data format that cannot express code).

The same attacker value can take two routes into an interpreter. Only one of them lets it change what the interpreter runs.

Interpreting input: the value either fills a slot or rewrites the structure
flowchart LR
  AT(["Attacker value"])
  CAT["<b>Concatenated into the code</b><br/><small>SQL string, HTML, shell string, path</small>"]
  SEP["<b>Passed separately</b><br/><small>bound parameter, escaped output, argv, allow-list</small>"]
  DB[("Database")]
  WEB("Browser")
  SH("Shell")
  FS("File system")
  AT --> CAT
  AT --> SEP
  CAT -->|"structure changed"| DB
  CAT --> WEB
  CAT --> SH
  CAT --> FS
  SEP -.->|"data stays data"| DB
  class CAT warn
  class SEP good
classDef neutral fill:#faf9f5,stroke:#5f5e55,color:#141413,stroke-width:1.5px
classDef compute fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef memory fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef queue fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef io fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef default fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
classDef warn fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:2px,stroke-dasharray:5 3
classDef good fill:#e2efd8,stroke:#2b6a24,color:#141413,stroke-width:2px

The entries that do not fit the four are not less important; they are about what surrounds the code. Cryptographic failures belong to the cryptography course. Vulnerable components are a supply-chain problem, covered in dependencies and supply chain. Logging failures are about detection. Unrestricted resource consumption is largely a capacity design question, worked through in a rate limiter. For systems built around language models, a parallel set of lists exists, and is mapped in frameworks and maps.

The whole map in one view: the four root causes with the CWEs this course uses, and the entries that sit outside them.

Four root causes inside the code, and what surrounds it
mindmap
  root((Vulnerability classes))
    Missing authorization
      CWE-862
      CWE-639
    Interpreting input
      CWE-89 SQL
      CWE-79 XSS
      CWE-918 SSRF
      CWE-22 paths
      CWE-502 deserialization
    Trusting the client
      CWE-287
      CWE-352
    Unsafe defaults
      CWE-611 XXE
      CWE-306
    Outside the four
      Cryptographic failures
      Vulnerable components
      Logging and monitoring
      Inventory of routes

Code#

A triage helper. Each simulated finding records which request field carried the attacker’s value and what the server did with it. A rule table maps that to a CWE ID and a root cause, then groups findings by the fix that removes the whole class. The last line shows what happens when a finding fits no rule.

root_causes.pyPython
"""root_causes.py: eleven findings, eleven CWEs, four root causes.

Each simulated finding records where attacker data entered and what the server
did with it. A small rule table turns that into a CWE ID and a root cause, then
groups the findings by the fix that removes the whole class. Standard library
only; offline.
"""

from __future__ import annotations

from collections import defaultdict
from dataclasses import dataclass

# What the server did with the value -> (CWE ID, short CWE name, root cause)
RULES: dict[str, tuple[int, str, str]] = {
    "object key, no owner check":   (639, "Authorization Bypass Through User-Controlled Key", "missing authorization"),
    "privileged action, no check":  (862, "Missing Authorization", "missing authorization"),
    "concatenated into SQL":        (89, "SQL Injection", "interpreting input"),
    "written into HTML unescaped":  (79, "Cross-site Scripting", "interpreting input"),
    "fetched as a URL":             (918, "Server-Side Request Forgery", "interpreting input"),
    "joined onto a file path":      (22, "Path Traversal", "interpreting input"),
    "deserialized as objects":      (502, "Deserialization of Untrusted Data", "interpreting input"),
    "believed as identity":         (287, "Improper Authentication", "trusting the client"),
    "cookie taken as intent":       (352, "Cross-Site Request Forgery", "trusting the client"),
    "parser default resolves XXE":  (611, "XML External Entity Reference", "unsafe defaults"),
    "endpoint open by default":     (306, "Missing Authentication for Critical Function", "unsafe defaults"),
}

FIXES = {
    "missing authorization": "one policy check on every function and every object, deny by default",
    "interpreting input": "keep data out of code: parameterize, encode, allow-list, safe parsers",
    "trusting the client": "derive identity, intent and state on the server",
    "unsafe defaults": "secure configuration as code, verified in CI",
}


@dataclass(frozen=True)
class Finding:
    fid: str
    where: str  # endpoint
    source: str  # which request field carried the attacker's value
    sink: str  # what the server did with it; a key of RULES


FINDINGS = [
    Finding("F01", "GET /api/orders/{id}", "path id", "object key, no owner check"),
    Finding("F02", "DELETE /api/users/{id}", "any session", "privileged action, no check"),
    Finding("F03", "GET /api/search?q=", "query string", "concatenated into SQL"),
    Finding("F04", "PUT /api/profile", "display_name", "written into HTML unescaped"),
    Finding("F05", "POST /api/avatar", "avatar_url", "fetched as a URL"),
    Finding("F06", "GET /download?file=", "query string", "joined onto a file path"),
    Finding("F07", "any request", "session cookie", "deserialized as objects"),
    Finding("F08", "any request", "X-User-Id header", "believed as identity"),
    Finding("F09", "POST /api/email", "ambient cookie", "cookie taken as intent"),
    Finding("F10", "POST /api/import", "XML body", "parser default resolves XXE"),
    Finding("F11", "GET /debug/vars", "nothing", "endpoint open by default"),
]


def classify(f: Finding) -> tuple[int, str, str]:
    try:
        return RULES[f.sink]
    except KeyError:
        raise ValueError(f"{f.fid}: no rule for {f.sink!r}; triage by hand") from None


def main() -> None:
    groups: dict[str, list[str]] = defaultdict(list)
    cwes: set[int] = set()
    print(f"{'id':<4} {'endpoint':<23} {'entered via':<17} {'CWE':<8} root cause")
    for f in FINDINGS:
        cwe, _name, cause = classify(f)
        cwes.add(cwe)
        groups[cause].append(f"{f.fid}/CWE-{cwe}")
        print(f"{f.fid:<4} {f.where:<23} {f.source:<17} {'CWE-' + str(cwe):<8} {cause}")

    print(f"\n{len(FINDINGS)} findings, {len(cwes)} distinct CWEs, {len(groups)} root causes\n")
    for cause, items in sorted(groups.items(), key=lambda kv: (-len(kv[1]), kv[0])):
        print(f"{cause} ({len(items)}): {', '.join(items)}")
        print(f"    fix once: {FIXES[cause]}")

    try:
        classify(Finding("F12", "POST /api/coupons", "coupon code", "applied twice in a race"))
    except ValueError as e:
        print(f"\n{e}")


if __name__ == "__main__":
    main()

Run it with python3 root_causes.py:

Text
id   endpoint                entered via       CWE      root cause
F01  GET /api/orders/{id}    path id           CWE-639  missing authorization
F02  DELETE /api/users/{id}  any session       CWE-862  missing authorization
F03  GET /api/search?q=      query string      CWE-89   interpreting input
F04  PUT /api/profile        display_name      CWE-79   interpreting input
F05  POST /api/avatar        avatar_url        CWE-918  interpreting input
F06  GET /download?file=     query string      CWE-22   interpreting input
F07  any request             session cookie    CWE-502  interpreting input
F08  any request             X-User-Id header  CWE-287  trusting the client
F09  POST /api/email         ambient cookie    CWE-352  trusting the client
F10  POST /api/import        XML body          CWE-611  unsafe defaults
F11  GET /debug/vars         nothing           CWE-306  unsafe defaults

11 findings, 11 distinct CWEs, 4 root causes

interpreting input (5): F03/CWE-89, F04/CWE-79, F05/CWE-918, F06/CWE-22, F07/CWE-502
    fix once: keep data out of code: parameterize, encode, allow-list, safe parsers
missing authorization (2): F01/CWE-639, F02/CWE-862
    fix once: one policy check on every function and every object, deny by default
trusting the client (2): F08/CWE-287, F09/CWE-352
    fix once: derive identity, intent and state on the server
unsafe defaults (2): F10/CWE-611, F11/CWE-306
    fix once: secure configuration as code, verified in CI

F12: no rule for 'applied twice in a race'; triage by hand

Eleven findings, eleven distinct CWEs, and four fixes. Five of the eleven are one bug in five interpreters. Written up as eleven tickets, they would be fixed one at a time by different people; grouped by root cause, they become a handful of changes to shared code, such as a query layer that only accepts bound parameters or a URL fetcher that only reaches allow-listed hosts. The coupon race (F12) has no rule on purpose: business-logic flaws do not fit the lists well and need a human to name them.

The program’s grouping as a chart: eleven tickets on the left of the output collapse into four bars, and one bar holds almost half the findings.

Eleven findings, four fixes: interpreting input alone covers five
xychart-beta
  title "Findings per root cause in root_causes.py"
  x-axis ["interpreting input", "missing authorization", "trusting the client", "unsafe defaults"]
  y-axis "findings" 0 --> 6
  bar [5, 2, 2, 2]

Where it breaks#

  • One finding can have several root causes. SSRF is interpreting input (a URL decides where the server connects) and also missing authorization (the server never asked whether this user may make it reach that host). The table picks one; a review should consider both, because each suggests a different control.

    One SSRF finding, read both ways. Each reading leads to a different control, and a review wants both.

    SSRF has two root causes, and each suggests its own control
    flowchart LR
      F5["<b>F05 POST /api/avatar</b><br/><small>avatar_url fetched by the server</small>"]
      R1["<b>Interpreting input</b><br/><small>a URL decides where the server connects</small>"]
      R2["<b>Missing authorization</b><br/><small>may this user make it reach that host?</small>"]
      C1["<b>Allow-list of destinations</b>"]
      C2["<b>Check the requester, not the deputy</b>"]
      F5 --> R1 --> C1
      F5 --> R2 --> C2
      class F5 warn
      class R1,R2 neutral
      class C1,C2 good
    classDef neutral fill:#faf9f5,stroke:#5f5e55,color:#141413,stroke-width:1.5px
    classDef compute fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
    classDef memory fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
    classDef queue fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
    classDef io fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
    classDef default fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:1.5px
    classDef warn fill:#f9e2e9,stroke:#b3154f,color:#141413,stroke-width:2px,stroke-dasharray:5 3
    classDef good fill:#e2efd8,stroke:#2b6a24,color:#141413,stroke-width:2px
  • CWE mapping is judgment. Two analysts can map the same bug to a parent class or a specific child. A header that sets identity could be filed as CWE-287 or as authentication bypass by spoofing (CWE-290). Pick a convention for your team and keep to it, so trends mean something.

  • The rule table is a toy. Real triage starts from a scanner report or a penetration test write-up, not from a neatly labeled sink field. The hard part is deciding what the server did with the value; once that is known, the mapping is easy.

  • The lists lag and skew. They are built from what was found and reported, so they favor what tools detect. Authorization flaws and business-logic abuse are more common in real applications than any count of CVEs suggests.

  • A CWE ID says nothing about severity. A reflected XSS on a static marketing page and a stored XSS in an admin console share CWE-79. Severity comes from context: who can trigger it, what it reaches, as in the ranking of the previous lesson.

  • Covering the Top 10 is not being secure. Passing a checklist of ten categories leaves out everything the categories do not name. Use the lists to check coverage and to speak a common language, not to define scope.

Remember this#

  • OWASP Top 10: web risk categories for awareness. API Security Top 10: API risks, three of them authorization. CWE Top 25: ranked weakness types from CVE data.
  • Tag findings with CWE IDs for precision; group them by root cause for fixes.
  • Four root causes cover most of what an application team writes: missing authorization, interpreting input, trusting the client, unsafe defaults.
  • Interpreting input is any path where data can change the structure of what an interpreter runs.
  • CWE-862 means no check; CWE-863 means a wrong check.
  • Severity comes from context, not from the category.

Try it#

  1. Run root_causes.py. Add a rule for "argument passed to a shell" mapped to CWE-78 (OS command injection) and a finding that uses it. Which group grows?
  2. Add a second root cause to F05 (SSRF) so that a finding can belong to more than one group. How does the summary change, and is the count of root causes still meaningful?
  3. Take the last security report you received, from a scanner, a penetration test, or a bug bounty. Tag each finding with a CWE ID and a root cause. How many distinct systemic fixes does it reduce to?
  4. For an API you know, go through the API Security Top 10 and write one line per entry: where it could occur, and which control you have today.

Check yourself#

  1. What is the difference between an OWASP Top 10 category and a CWE ID?

    Show answer

    A Top 10 category is a broad risk area that maps to many CWEs, chosen for awareness and ranked from testing data and a survey. A CWE ID names one specific weakness type in a maintained catalog, such as CWE-89 for SQL injection. Categories are good for communication and coverage; CWE IDs are good for precise tagging and trend analysis.

  2. Why do three of the ten OWASP API Security risks concern authorization?

    Show answer

    APIs expose object IDs, field names, and functions directly to the caller, and every one of them is a key the caller can change. Each must be checked against the caller’s authority: the object (API1), the properties within it (API3), and the function itself (API5). Missing any one of these checks is enough for an attacker who can edit a request.

  3. SQL injection, cross-site scripting, path traversal, and SSRF share a root cause. What is it, and what does the shared fix look like?

    Show answer

    Interpreting input: attacker data reaches an interpreter (the database, the browser, the file system, the network stack) in a position where it can change the structure of what is interpreted. The fix keeps the structure fixed and passes data separately: bound parameters, contextual output encoding, path resolution checked against a base directory, and an allow-list of destinations.

  4. A route has an authorization check that compares the caller’s tenant with the tenant in the request body instead of the tenant that owns the object. Is that CWE-862 or CWE-863?

    Show answer

    CWE-863, Incorrect Authorization. A check exists, but it tests the wrong thing: a value the client supplied rather than the owner recorded on the server. It is also an instance of trusting the client, which is why it passes casual review.

  5. Why is the CWE Top 25 a poor guide, on its own, to what to fix first in a Python web service?

    Show answer

    It is ranked across all software, so memory-safety weaknesses from C and C++ code rank high but rarely apply directly. It is also built from reported CVEs, which underrepresent authorization and business-logic flaws in private applications. Prioritize from your own threat model and findings, and use the list as a cross-check.

Sources#