CWE Top 25: What Engineers Actually Need to Know

The CWE Top 25 Most Dangerous Software Weaknesses is a list most developers have heard of but few have actually read through with any intention of using it. That is partly because the official documentation presents it as a taxonomy — definitions, identifiers, and relationships between weakness categories — rather than as a practical guide to what these patterns actually look like in a diff.

We review a lot of code. The same CWE categories appear repeatedly, in different languages, different frameworks, different team sizes. This guide focuses on the twelve entries that show up most often in real pull requests, with examples grounded in the patterns we actually see, not the textbook versions.

The Rankings and What They Mean

MITRE publishes the Top 25 annually, weighted by CVE prevalence data from NVD. The rankings shift year to year, but the top five are remarkably stable. As of the 2024 data:

Rank CWE ID Weakness Name Typical CVSS
1 CWE-79 Cross-site Scripting (XSS) 6.1–8.8
2 CWE-787 Out-of-bounds Write 7.8–9.8
3 CWE-89 SQL Injection 7.5–9.8
4 CWE-416 Use After Free 7.8–9.8
5 CWE-78 OS Command Injection 9.8
7 CWE-125 Out-of-bounds Read 7.1–9.1
8 CWE-22 Path Traversal 5.3–9.1
11 CWE-476 NULL Pointer Dereference 5.5–7.5
14 CWE-502 Deserialization of Untrusted Data 7.8–9.8
20 CWE-306 Missing Authentication for Critical Function 9.1
23 CWE-434 Unrestricted Upload of File with Dangerous Type 7.5–9.8
25 CWE-362 Race Condition 7.0–8.1

The web application developer's lens is focused on CWE-79, CWE-89, CWE-78, CWE-22, CWE-502, CWE-306, and CWE-434. Memory safety weaknesses (CWE-787, CWE-416, CWE-125, CWE-476) matter primarily for C, C++, and Rust codebases.

CWE-79: Cross-Site Scripting

XSS tops the list because it is ubiquitous. The pattern: user-controlled content gets rendered into a browser DOM without escaping, allowing an attacker to inject JavaScript that runs in the victim's browser context.

The three XSS subtypes have different attack surface characteristics. Reflected XSS requires the attacker to deliver a crafted URL to the victim. Stored XSS (content stored in the database and rendered later) is more dangerous because it requires no victim interaction beyond loading the page. DOM-based XSS occurs when JavaScript reads a value from a user-controlled source (URL fragment, cookie, localStorage) and writes it to the DOM without sanitization — automated scanners frequently miss this subtype.

In React codebases, the primary XSS vector is dangerouslySetInnerHTML. If you see this in a PR diff, verify that the value being rendered is either entirely developer-controlled (not from user input or database content) or passed through a sanitizer like DOMPurify with a strict allowlist. A single unguarded dangerouslySetInnerHTML set to user-controlled content is a full XSS vulnerability.

CWE-89: SQL Injection

SQL injection is the entry in the Top 25 most likely to appear as a high-severity finding in a real codebase. It is also the entry with the most straightforward prevention: parameterized queries, everywhere, without exception.

The patterns we see most often in code review that suggest SQLi risk: any ORM .raw() or .execute() call that accepts a string with variable interpolation, any use of Python's f-strings or string .format() method to construct SQL, and any JavaScript template literal building a database query string. These are often legitimate in developer intent but wrong in implementation.

The fix is always the same: replace string interpolation with parameterization. db.execute(f"SELECT * FROM users WHERE id = ${userId}") becomes db.execute("SELECT * FROM users WHERE id = %s", [user_id]). The pattern is simple enough that a one-line linter rule banning SQL string concatenation catches the majority of occurrences.

CWE-78: OS Command Injection

OS command injection occurs when user-controlled input reaches a shell execution context. In Python, this means os.system(), subprocess.run(shell=True, ...), or subprocess.Popen() called with a string argument built from user input. In Node.js, it means child_process.exec() with string interpolation.

The canonical fix: use argument arrays instead of shell strings. subprocess.run(["grep", user_input, filename]) never passes user input to a shell interpreter regardless of what characters the input contains. The shell metacharacters (;, |, &&, $()) are passed as literal arguments to the program, not interpreted by the shell.

From our code review data: OS command injection is disproportionately found in DevOps automation scripts and internal tooling — code written quickly without security scrutiny, often not covered by automated scanning because it lives outside the main application repository. These are exactly the repositories that benefit most from bringing into a centralized security review pipeline.

CWE-22: Path Traversal

Path traversal (also called directory traversal) occurs when user-controlled input is used to construct a file path, and the application fails to constrain the resulting path to an intended directory. The attack pattern: provide ../../etc/passwd as a filename parameter, and the application reads the system password file instead of the intended resource.

In Python, the prevention is to resolve the user-provided path and verify it starts with the intended base path:

import os
BASE_DIR = '/var/app/uploads'
user_path = request.args.get('file')
resolved = os.path.realpath(os.path.join(BASE_DIR, user_path))
if not resolved.startswith(BASE_DIR + os.sep):
    return abort(403)
with open(resolved) as f:
    return f.read()

Without the startswith check, a user-provided path of ../../etc/shadow will resolve outside the base directory and be served. This pattern catches 100% of path traversal attempts, including those using null bytes, URL encoding, and Unicode normalization tricks.

CWE-502: Deserialization of Untrusted Data

Insecure deserialization received significant attention after the Apache Commons Collections exploit chain was published in 2015, and it remains a high-severity entry because the impact is typically remote code execution. The pattern: an application deserializes data from an untrusted source (a cookie value, a request body, a cache entry that could be poisoned) using a deserializer that can instantiate arbitrary object types.

In Python, the canonical dangerous call is pickle.loads(user_data). Python's pickle module can deserialize data that triggers arbitrary code execution as a side effect of object construction. Never deserialize pickle data from an untrusted source. Use JSON, protobuf, or a schema-validated format instead.

In Java, the analogous pattern is Java's native serialization (ObjectInputStream.readObject()) and XML deserialization via XStream with the default configuration. Both have well-documented exploit chains; neither should be used to deserialize data from external sources without explicit type allowlisting.

CWE-306: Missing Authentication for Critical Function

This one seems obvious but appears in practice more often than you would expect, typically through one of two mechanisms: a developer adds a new API endpoint and forgets to add the authentication middleware, or an existing endpoint has authentication added at the route level but a new code path bypasses it.

The failure mode we see most often in code review: a new internal API endpoint created for a background job or administrative function that is not meant to be public-facing but gets deployed with no authentication because "it will only be called by our own systems." Internal endpoints without authentication are routinely discovered by attackers who scan for common administrative paths, and the "only internal systems call this" assumption is only as strong as your network segmentation — which is usually weaker than teams believe.

CWE-434: Unrestricted File Upload

File upload endpoints are consistently underspecified in security terms. The minimal safe file upload implementation validates: file size limit enforced before reading content, MIME type validated from file content (using python-magic or equivalent, not the Content-Type header), file extension validated against an explicit allowlist, and the stored file is not accessible from a web-servable path that executes scripts.

A file upload endpoint that stores files in a directory served by your web server without extension restrictions is a webshell upload vulnerability. An attacker uploads a PHP file with a .jpg extension (a Content-Type header claim the server trusts), the server serves it, and the PHP interpreter executes it.

Applying the CWE Top 25 in Your Workflow

The most practical application is treating this list as a categorization framework rather than a reading list. When your SAST tool surfaces a finding, knowing its CWE ID immediately tells you the severity class and the canonical prevention. When you are reviewing code that handles file paths, user input, or serialized data, knowing which CWEs apply to those operations focuses your attention on the right questions.

For teams running automated code review, mapping tool findings to CWE IDs also makes compliance reporting tractable. OWASP Top 10 categories align closely with CWE clusters — A03 (Injection) maps to CWE-89, -78, -79; A04 (Insecure Design) includes CWE-306; A08 (Software and Data Integrity) maps to CWE-502. If your SAST tool tags findings with CWE identifiers, your sprint-end security report becomes a CWE coverage summary rather than an unstructured list of raw findings.

The CWE Top 25 changes annually. The pattern it reveals does not: a small number of weakness categories account for the overwhelming majority of exploited vulnerabilities. Getting good at recognizing those patterns in code review — whether automated or manual — is the single most efficient security investment most engineering teams can make.

PR Security Review Checklist Shift-Left Security Implementation Guide