How to read a CVE properly
A CVE record is the address of the bug, not the bug itself. Where to find the patch, the advisory, the exploit, and which order to read them in.
A CVE entry is a phone number for a bug. It tells you who to call, roughly when the call should happen, and almost nothing about why the call matters. The vulnerability itself lives somewhere else — three or four places that the CVE record doesn't always link in any helpful order. Knowing where to look, and what to read first, is most of the skill.
What's in a CVE record
The MITRE entry has a fixed shape. An identifier (CVE-YYYY-NNNNN), a one-paragraph description usually written by a vendor PR person under time pressure, a CVSS score almost certainly assigned by someone who didn't read the patch, and a list of references. The references are gold; the description is filler.
A typical description reads "A flaw was found in libfoo. A specially crafted input could allow an attacker to execute arbitrary code." That tells you nothing. What does "specially crafted" mean? Where is the parser? Stack overflow, heap overflow, type confusion, logic bug? Is "execute arbitrary code" achievable in practice or theoretical against modern mitigations? The CVE record exists to be a stable identifier the ecosystem can hang information off, not to teach you the bug.
What's not in a CVE record
The bug class. The trigger condition. Whether ASLR, stack canaries, or CFI break the exploit primitive. Whether the same root cause shows up elsewhere in the codebase. The history of how the bug was introduced. The constraints an attacker has to satisfy to reach the vulnerable code path. None of that is in the CVE itself.
Most of it is in the patch.
Finding the patch
If the project is open source, the patch is your single best source. Walk through the references on the CVE entry looking for one of these:
- A link to the upstream repository's commit page, usually
github.com/<org>/<repo>/commit/<hash>or a Git web UI on a self-hosted forge. - A link to the project's own security advisory, which typically embeds the commit hash.
- A link to a distribution's bug tracker (Red Hat Bugzilla, Debian's security tracker), which often links the patch even when the upstream advisory does not.
If none of those work, try git log --grep=CVE-YYYY-NNNNN on the upstream repo. Many projects mention the CVE ID in the commit message of the fix or in the next release's changelog. For Linux kernel CVEs, git.kernel.org and the Greg KH stable trees are the canonical reference.
For closed-source software, you usually can't read the patch directly. The next best thing is the vendor's bulletin (Microsoft MSRC, Apple security release notes, Adobe advisories) plus any third-party binary-diff writeup — researchers like watchTowr or Project Zero occasionally publish post-patch teardowns built with BinDiff or Diaphora.
Reading the patch
Open the diff. First question: which file changed? That tells you which subsystem the bug lived in. Second: what's the smallest change that actually fixes the bug? Patches often touch several files for refactoring or test additions; the load-bearing part is usually a handful of lines that add a bounds check, fix an integer comparison, change a free order, or add a NULL check before a deref.
Look for added lines that resemble:
if (len > MAX_FOO || len < sizeof(struct hdr)) return -EINVAL;or
if (idx >= ARRAY_SIZE(table)) goto err;Those are the shape of "the previous code didn't validate this and could be made to do something bad". Read the surrounding function and ask: what would have happened, before the patch, if the new check fired? An unchecked value flowing into a memcpy length is a memory-corruption primitive. Into an array index is an OOB read or write. Into a function-pointer load is control flow.
The patch is also the easiest place to spot a bad fix. A check added three layers up from the actual bug is sometimes incomplete. A patch with a new sanity check but no test exercising the old broken path should make you wonder whether the author fully understood the bug.
Finding the original advisory
The advisory tells you the story. Impact assessment, CVSS reasoning, affected versions, sometimes the trigger primitive in plain English. Different vendors put advisories in different places:
- Open-source projects: a
SECURITY.md, a GitHub Security Advisory (GHSA), aCHANGELOG, or a mailing list post (oss-security on openwall is a clearing house). - Linux distributions: Debian DSA, Ubuntu USN, Red Hat RHSA.
- Big vendors: each has their own portal, and you'll learn the URLs by repetition.
Advisories are usually short. Read twice. The parts worth re-reading are the prerequisites for exploitation (authentication? local access? specific config?) and the workaround section, which often hints at the trigger.
Reading order
In that order. The CVE points you at the references, the advisory frames what the bug does, the patch shows what was wrong, and a reputable PoC (if one exists) confirms whether your understanding of the primitive holds up.
When to read the exploit (and when not to)
Read it when you've understood the patch but still can't see how the bug becomes an attacker primitive, when you're studying technique rather than the specific bug, or when you're verifying that a fix really does close the vector you think it does.
Skip it when you haven't read the patch yet — you'll absorb recipes instead of principles. Skip it when the PoC came from a drive-by GitHub account, since "PoC" repos are sometimes malware-laden bait for analysts. Skip it entirely if you only need to decide whether to patch; the advisory answers that.
A worked example: CVE-2021-44228 (Log4Shell)
The CVE description: "Apache Log4j2 ... JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints." Accurate, not illuminating.
The Apache advisory adds the part you actually need: any string controlled by an attacker that ends up inside log.info(...) or similar gets parsed for the substring ${jndi:ldap://...}, which Log4j resolves by performing a JNDI lookup. The lookup can be made to fetch a remote class and instantiate it.
The patch (commit c77b3cb) disables JNDI lookups by default and tightens the substitution lookup. Read the diff and the bug class snaps into focus — an unintended-feature bug, not memory corruption. Someone wired a string interpolation hook into the logging path that nobody had thought through as an attack surface. The exploit ends up being a string. Every web form, User-Agent header, and chat message that ever fed into a Log4j log line was attacker-controlled input to that interpolator.
That's the kind of insight you only get from reading the advisory and the patch together. The CVE entry will never give it to you.
Where this fits in
The full pipeline — finding bugs, triaging them, writing advisories, tracking patches — is covered in Module 95: Zero-Day Discovery Workflow. Reading other people's CVEs well is the habit that makes finding your own much easier; the instincts you build on fixed bugs are the same instincts that fire when you're staring at a fresh codebase wondering where the next one lives.