Agustin Barrientos
All posts

How to Read a Security Advisory: CVE, GHSA, CWE, and CVSS in Plain Words

Want more of these in your Google results?

Two official databases looked at the same bug in Vite and disagreed about how bad it is. GitHub’s advisory record scores it 5.3 and labels it “Moderate”. The vulnerability database run by the United States government (the NIST NVD) scores the same CVE 7.5 and labels it “High”, using the same scoring standard.

Neither one is wrong, and neither one knows a thing about your project. A severity score compresses somebody else’s assumptions about how an attack would go, and your system is what confirms or breaks those assumptions. That gap is the best argument I know for reading the rest of the record.

An advisory is a short, structured record about a bug in code you depend on. One reaches you as a Dependabot alert, a line of npm audit output, or a link a teammate drops in the channel with “should we care?”. Most of us were never taught to read one, so we read the part that looks readable, the label, and let a single word make the call.

So we’re going to read that Vite advisory end to end, and turn each of its fields into a check you can run against your own project. One question carries the whole discipline:

What has to be true for this bug to hurt our system, and which of those conditions can I check myself?

Everything below is that question applied one field at a time, and it transfers to every alert you’ll ever triage.

What you’ll take away

The example below is a Vite bug, but none of this is about Vite. By the end you should be able to do all of this with whatever advisory turns up in front of you.

  • Name the affected package and the exact version you actually have installed, from the lockfile rather than package.json.
  • Read CVE, GHSA, CWE, and CVSS as four different answers instead of four synonyms for danger.
  • Say what the bug does in one plain sentence.
  • Read the assumptions behind a severity score, and explain how two official databases can score one bug differently.
  • Work out whether the broken code is reachable where you run it, and by whom.
  • Tell an honest mistake from a deliberate attack, and change your response when it’s the second.
  • Decide whether an update waits a few days or ships today through a named exception.

Two teams further down install the exact same vulnerable version, and one of them should patch today while the other can safely wait for the normal update. You’ll be able to say why, without hedging.

None of this needs a security background, and there’s nothing to set up before you start. Every term gets explained the first time it matters, lockfiles and version ranges included, and the figures are there to be poked at rather than studied. You don’t even need an alert waiting for you, and the calmest time to learn this is before one lands.

A quick note on letting AI do this

Point an AI at this work, absolutely. Paste an advisory into a chat and it will summarize the bug in plain words, compare the affected ranges against your lockfile, and draft the update plan faster than you can open the tabs. The gap between a generic answer and a genuinely good one is the evidence you ask for. Did it check the exact version your lockfile resolves or just the range, did it verify the advisory’s precondition against your actual config, and does it know who can reach the affected code in your deployment? Knowing that those are the questions is the skill this guide teaches, and it matters more as the models improve, because a fluent summary is now free and a verified conclusion still isn’t.

Working through it

The six steps below follow the order I actually use. The first three stay inside the record, and the last three move into your repository, where the decision lives.

Step 1: Read the record in a useful order

Our specimen is GHSA-x574-m823-4x7w, a real 2025 advisory about Vite, the tool that serves your site on your own machine while you work on it and bundles it when you ship. Read top to bottom, an advisory is a wall of jargon. Read in the right order, it becomes a short answer to the first half of our question, the “what has to be true” half.

Start with the package and the versions. The record names vite on npm, then answers the only question that matters this early, which versions carry the bug and which ones don’t.

A package like Vite is kept alive in several release lines at once, meaning the 6.2.x, 6.1.x, 6.0.x, 5.x and 4.x families that different projects happen to be sitting on. The record gives each line its own broken range and its own first fixed version. On the 6.2 line, everything from 6.2.0 up to and including 6.2.2 has the bug, and 6.2.3 is the first version that doesn’t.

So the reading is blunt. Find the release line you’re on. If your version sits at or below that line’s broken top, you have the bug. If it’s at or above that line’s fix, you don’t.

This record covers five release lines and gives each one its own fix, which is the maintainers being kind. The highest number on the page, 6.2.3, is only the right answer for someone already on the 6.2 line. Someone on 4.x takes 4.5.10 instead, a small patch on the line they’re already sitting on, rather than jumping two major versions and rewriting their build to get safe.

One notation quirk trips people up, and it comes from the same bug being written down in more than one place. The maintainer publishes the advisory, and the big databases each keep their own copy of it, which is what people mean by a mirror. The maintainer wrote the top of each broken range as <=6.2.2, meaning “6.2.2 and everything before it”. The mirror in the GitHub Advisory Database rewrote the same thing as <6.2.3, meaning “everything before 6.2.3”. Those two describe an identical set of versions, so meeting both isn’t a contradiction.

Then read what the bug does, in the record’s own words, “the contents of arbitrary files can be returned to the browser.” Arbitrary is the load-bearing word there, and it means any file that process can read, not just the ones your site is meant to serve, so the .env file where people keep keys is fair game. The mechanism sits further down, in the Details section. A URL suffix, ?raw?? or ?import&raw??, slips past server.fs.deny, the option that tells the dev server which files it must refuse to serve. If you can retell that in your own words and name the code of yours that runs it, here the dev server rather than your production bundle, you understand the bug well enough to triage it.

Now the sentence juniors skim and seniors read twice, the extra condition the bug depends on. Verbatim from the record, “Only apps explicitly exposing the Vite dev server to the network (using --host or server.host config option) are affected.”

In plain words, the dev server is the thing that starts when you run npm run dev, and out of the box only your own machine can open it. Passing --host, or setting server.host in the config file, tells it to answer other machines as well, which is what people do so a colleague or a phone on the same wifi can see the site. If nobody turned that on, nobody outside your laptop can reach the bug at all, and this whole advisory is a scheduling problem rather than an emergency.

The precondition does more work in this guide than the severity label does. Notice too where the maintainer filed it, under the Impact heading rather than anywhere obvious. These body sections are hand-written prose, so trust what a section says over what its heading promises.

Two more things sit at the bottom of the record, and both change how you act on it.

The first is the proof of concept, which is working attack instructions the reporter includes to show the bug is real rather than theoretical. On this record it’s a single curl command anyone can copy and run. That should move your timing, because an attack with public instructions needs no skill and no preparation, so if anyone outside can reach your dev server you should assume somebody eventually tries it.

The second is the dates. This record went up in March 2025, and the copy of it at the United States government’s database was still being edited in 2026 when I checked. Records get corrected, re-scored, and re-worded long after they first appear, and the copies sitting in other databases don’t always keep up. So when you’re about to act on one, open the maintainer’s own version rather than whichever copy your scanner happened to link.

Walk that whole reading order in the figure below. The tour fills in a ledger as it goes, and the interesting column is the one on the right, the questions the record never answers no matter how carefully you read it.

A mock of the advisory page for GHSA-x574-m823-4x7w, built for this article, not the real page. Click any numbered field or step through all nine (arrow keys work); the strip says what each field can and cannot tell you.

Step 2: Four codes, four different questions

The record wears a small pile of codes, and they aren’t four flavours of the same thing. Two of them are names for this exact bug, one is the category of mistake behind it, and one is a score. None of them answers “how scared should I be”.

Table The four codes on an advisory, and what each one is actually for.
CodeWhat it isWho assigns itWhat it can’t tell you
CVE-2025-30208Common Vulnerabilities and ExposuresThis bug’s name in the one catalog everybody shares, so two tools can prove they mean the same bugAn organization authorized to hand out those names, here GitHub. Microsoft, Google and Red Hat each run their ownHow bad it is, or whether it affects you
GHSA-x574-m823-4x7wGitHub Security AdvisoryThe same bug’s name inside GitHub’s own database, the copy Dependabot readsGitHubAnything the record itself doesn’t say
CWE-200 and CWE-284Common Weakness EnumerationThe kind of mistake this is, here showing information to people who shouldn’t see it, and access control that doesn’t holdA community-run catalog of about a thousand recurring mistakesWhether this particular bug is a problem for you
CVSS 5.3 and 7.5Common Vulnerability Scoring SystemA severity score out of 10, plus the assumptions the scorer made to reach itWhoever did the scoring, and here two of them landed differentlyYour own risk, which only your setup can settle
A CVE is a shared name, not a database. The CVE program exists so that GitHub, the scanners, and your teammate can all point at the same bug and know it.

Two details from that table are worth a sentence each. The year in a CVE is when the identifier was assigned, or when the bug went public if that came first, and it says nothing about the day anyone discovered the problem. The full ID never changes once assigned, so treat it as a lookup key and read everything else out of the record it points to.

Who hands the IDs out surprises people. There’s no single office issuing them. The CVE program instead authorizes about a hundred organizations, each called a CVE Numbering Authority or CNA, to hand out IDs themselves.

Each one can only name bugs in the software it’s responsible for. Microsoft can name a bug in Windows and Red Hat one in its own Linux, and neither of them can do it for Vite. Plenty of open source projects hold that authority for their own code as well, curl among them.

GitHub is a CNA whose reach covers public repositories it hosts, as long as no other CNA already covers that project. That’s why a maintainer can file an advisory on their own repository and have a CVE attached to it, which is exactly how this Vite record got its number.

GHSA is GitHub’s own prefix, and most ecosystems that keep a database have one of their own. Python advisories arrive as PYSEC, Rust as RUSTSEC, Go as GO, Red Hat as RHSA, and a package caught being deliberately malicious gets a MAL id. They all carry the same shape of record, so learning to read this one teaches you the rest, and OSV is the format most of them are gathered into.

Both CWE entries link out to a page of their own, CWE-200 and CWE-284, describing the pattern and the usual ways people walk into it. That’s what the CWE list is for, learning across many bugs rather than responding to one. The record you actually respond to is still the advisory.

One habit follows from all those copies. When two sources disagree about a bug, follow the references upward toward the maintainer’s own advisory, because that record is the one everything else summarizes. A screenshot going around social media is how you learn a problem exists, never what you patch from.

What actually matters day to day is smaller than that table suggests.

  • The two names (CVE and GHSA) are lookup keys, so paste one into a search box when you need the record and forget them the rest of the time.
  • The weakness family (CWE) names the kind of mistake behind the bug, which is worth reading when you want to learn the pattern, and it never changes whether you’re affected or how fast you need to move.
  • The score (CVSS) is the only one of the four that will tempt you into acting on it, and it’s also the one that knows least about your project.

The score is the one worth arguing about, so it gets the next step to itself.

Step 3: The score describes the bug, not your risk

Back to the disagreement this guide opened on. One record scores the bug 5.3 and calls it “Moderate”. The analysts at the United States government’s database score the very same bug 7.5 and call it “High”.

A score is a summary of eight assumptions about how an attack would go. Every score ships with those eight assumptions written out beside it, and on this bug the two records differ in exactly two of them.

The first is how much has to line up before the attack works, where one record says quite a lot and the other says almost nothing. The second is whether a real person has to do something first, like clicking a bad link, where one record says yes and the other says no. One team imagined a fussier attack than the other, and that judgment call is the entire 2.2-point gap.

Neither number is wrong, and neither number knows anything about you. FIRST, the organization behind the scoring standard, says so in a heading in its own user guide. The score measures how bad the bug is, not how much danger you’re in. It describes the bug on its own, with no idea what your project looks like. Your danger only shows up once you add the things the score can’t see, like whether the broken feature is even switched on for you, what the affected code could read, and whether anyone is attacking it out there.

So when two sources disagree, don’t just take the scarier number. Read both sets of assumptions and ask which ones are true where you work.

The figure below opens on this bug’s two real records with the two differences already highlighted. Paste in the assumptions from any other CVSS 3.1 advisory and it reads that one too, which is why it’s worth keeping around.

The same Vite advisory, drawn as the two severity boxes the GitHub and NVD pages publish, 5.3 Moderate against 7.5 High. The wire between the boxes ties together the two rows where their answers differ. Click any row to read that question in plain words, or paste another advisory's CVSS 3.1 line into either box. Both boxes are mocks built for this article, not the real pages.

The words attached to those numbers are just bands, “Low” up to 3.9, “Medium” to 6.9, “High” to 8.9, “Critical” above that. A 6.9 and a 7.0 sit one rounding apart and wear different colors, which is one more reason to read the assumptions instead of the paint.

Step 4: Work out whether this actually affects you

Everything so far came out of the record. The other half of our question, the part you can check yourself, lives in your own project. I run it as seven questions in order, and I stop at the first no.

  1. Is the package in your project at all?
  2. Is the version you installed one of the broken ones?
  3. Does the broken code even run in this setup?
  4. Is the extra condition the advisory names true for you?
  5. Could someone you don’t trust reach it?
  6. Is there anything worth stealing within reach?
  7. Is there a fixed version for the line you’re on?

The lockfile answers the first two, and package.json doesn’t. The range in package.json says which versions you’d accept, while the lockfile records the one you actually got. One command prints the version you really have, plus who asked for it.

A mock terminal, built for the article. It types the command and prints the shape of output npm gives back, with the resolved version on the first line.

The installed 6.2.2 is one of the broken versions, so the first two answers are yes. Your own scripts and config settle question 4, and on this bug that’s a single search for --host and server.host. Whether anyone outside could actually reach your dev server settles question 5, and the advisory’s list of fixed versions settles the last one.

A single no means you don’t have to move today, and it doesn’t mean you can close the alert. Write the reason down where the next person will find it, because setups drift and nobody re-checks a reason nobody recorded. “Not affected because the dev server never leaves this laptop” survives a config review. A bare “dismissed” guarantees someone either argues about it again or trusts it blindly.

Now watch it play out. Two teams install the exact same vite@6.2.2. One runs npm run dev on their own machine only, so question 4 is a no and the fix can wait for the normal update. The other opened the dev server with --host so a designer could see the site from a phone, and every answer is yes, so that team patches today.

The third option in the figure is the tricky one, a production build, where question 3 is already a no because the dev server never runs in production. The urgency doesn’t vanish, though. It moves to the machines that do run the dev server, which are your team’s laptops.

The figure below runs those seven questions as a terminal session, one at a time, with all three setups sitting in the menu.

A pretend terminal built for this article, not a real shell. Pick a setup in the title bar to watch the seven questions run on their own, or pick Answer it myself and work them one at a time.

Step 5: Work out whether it’s a mistake or an attack

Everything above assumed the package is honest code with an accidental hole in it. That assumption deserves its own check, because the other case plays by completely different rules.

An accidental hole only hurts you if an attacker can reach it, which is exactly what the seven questions test. The other kind is code published on purpose to hurt whoever installs it, usually after someone steals a maintainer’s account or gets into the automated setup that publishes their releases. There’s nothing to walk through in that case, because installing it may already be the moment it got you. Some of that code runs during npm install, some runs the first time your build or your tests import the package, and either one is enough.

Two incidents from earlier in 2026 show how this actually plays out. In May 2026, an attacker got into the automated setup TanStack uses to publish releases and pushed out 84 malicious versions across 42 packages in a six-minute window, per the team’s own postmortem. Outside researchers flagged the wave within about 20 to 26 minutes, and npm’s registry, the servers every npm install downloads from, pulled the packages within hours.

The TanStack team still told everyone who installed one to change every password and key that machine could reach, cloud logins, npm tokens, and SSH keys included. Catching it fast doesn’t reach back into a laptop that already ran the install.

The Jscrambler compromise that July was smaller and even more instructive. In one afternoon the attacker shipped five malicious versions of the jscrambler package, plus one poisoned version each of its webpack, gulp, grunt, and metro plugins. The registry counted 1,479 downloads across all the affected versions in the roughly two hours they were live.

The first three of the five jscrambler versions ran their attack from an install script, which is a command a package can ask npm to run for it the moment it lands, before you have imported a line of it. The last two dropped that hook and put the same code at the top of the package’s entry files instead, so it fired the moment anything imported the package or ran its command. As Socket’s analysis puts it, that variant “survives npm install --ignore-scripts”. npm 12 now blocks dependency install scripts by default, which is real progress, and the second generation walks straight past it, because nothing about an import is an install script.

Sort a few real cases yourself before reading the answer, since the difference between a bug and an attack gets much easier to spot once you’ve been wrong about one.

A mock alerts inbox built for this article, not a real repository page. Open each alert, call it an honest mistake or a deliberate attack, and the row stamps the real answer with its source. The table at the bottom keeps the difference even if you skip the sorting.

So when an alert says the package was attacked rather than buggy, stop working through questions and switch to damage control. Don’t install or build from that state again, work out what the code could have reached, change those passwords and keys from a machine you trust, and hand it to whoever handles security incidents where you work. Deleting node_modules tidies up your project and undoes nothing the code already did.

Step 6: Pick the moment to update

Once you know which kind of problem it is, the decision is about timing, and two things pull in opposite directions. Waiting costs you, because a known bug stays live in your project the whole time, and plenty of these are public. Waiting also protects you, because a brand new release is the one most likely to be an attack, and both incidents above were spotted within minutes and pulled within hours. A release that has been public for a week has had a week of other people’s scanners, researchers, and unlucky build servers looking at it.

The senior move is deciding which of those two matters more for this particular update, and the everyday case can be automated. npm has a setting that makes every install skip anything published in the last few days, and as of the time of writing it’s one line in your config.

ini
# .npmrc
min-release-age=7

From then on every install takes only versions that have been public for more than seven days, and that covers the packages your own packages drag in behind them. Installing Vite, for instance, also installs rollup.js, esbuild, and PostCSS, because Vite depends on all three. If esbuild shipped a new version this morning, the wait applies to it too, so npm hands you the previous one instead, even though it isn’t a package you ever asked for. If nothing old enough fits, the install fails loudly instead of quietly grabbing something fresher.

The obvious question is which number to use. npm ships the setting switched off, so nothing waits until you say so, and its docs decline to suggest a figure. GitHub did pick one, settling on three days for Dependabot’s default because, in its words, “a short delay gives that signal time to surface”.

I’d start at seven. Both attacks above were pulled within hours rather than days, so even three clears the window they lived in, and seven leaves margin for the slower case nobody catches on day one. The cost is a week’s delay on routine updates, which is exactly what the named exception is for. Whatever you land on, write down why, so the next person can argue with the reasoning instead of the number.

The setting is honest about the cost, too. npm audit fix is the command that tries to install security fixes for you. When the wait blocks it from taking one that’s too new, npm keeps the broken version, warns you, and fails, which is exactly the conversation you want to be forced into. A security fix you’ve already reviewed is the one update that shouldn’t wait, so you name it as an exception.

sh
npm install vite@6.2.3 --min-release-age-exclude=vite

The exclude flag lets through exactly the one package you reviewed, and everything else keeps waiting. Keep the exception in the command or the pull request, where the next person can see which package skipped the wait and why.

Drag the slider in the figure below and watch both sides move at once. Any wait at all would have stopped the two attacks from step 5, since those versions were public for hours, and the same wait delays the security fix you actually want until you name it.

A mock editor and terminal built for this article; no real install runs. Drag the wait and the .npmrc number and the install output change together. Flip the toggle to let the reviewed Vite fix through by name while both attacks stay held back. The .npmrc lines are yours to copy.

Dependabot does this waiting for you already, and its three-day default is the number quoted earlier. That became the default in mid-July 2026 and applies only to routine update pull requests, since security fixes still open straight away. A cooldown block in dependabot.yml moves the window wherever you settled on.

yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    cooldown:
      default-days: 7
      semver-major-days: 14

A bot can only suggest, though. A green pull request proves your build still works, and it can’t tell you whether the advisory applies to you, whether the version suits the line you’re on, or whether the lockfile changed more than the title says. Those checks are yours, and they’re the seven questions from step 4 wearing a different hat.

One habit belongs in either case. Installing a package runs its install script, and testing it runs the package itself, so both are other people’s code running on your machine. Switching install scripts off closes one of those doors and leaves the other wide open.

So do that work somewhere that holds no deployment passwords or keys. Most teams install and test on a shared machine that runs automatically when you open a pull request, and plenty of those same machines also hold the keys used to deploy. Separating the two, so the deploying job only ever receives a finished build, buys you more safety than any amount of advisory reading.

On GitHub Actions that separation is close to one line. Put the deploy credentials in an environment instead of repository secrets. Only a job that opts into it can read them, and the one running npm install never should. Every other CI system has its own version of the same idea.

Notice who owns what by the end of this. The registries own the record, the names, the scores, and now the waiting. You own the seven questions, the reason you wrote down, and the decision, and no new platform feature is coming for those.

A few ways this goes wrong

  • Reacting to the label. Two official records put this one bug 2.2 points apart, so the label is hiding assumptions you haven’t checked yet. Read the assumptions and the extra condition before you read the color.
  • Ignoring it because you never installed that package yourself. The code runs no matter which of your packages asked for it. Who pulled it in changes how you apply the fix, and it says nothing about whether it affects you.
  • Merging the bot’s pull request because the build is green. A green build proves your code still compiles and your tests still pass. Whether the advisory applies to you, whether the version suits your line, and what else moved in the lockfile are things the build never looked at.
  • Thinking uninstalling is enough after an attack. Removing the package cleans up your project and undoes nothing that already ran. If the code ran, the passwords, keys, and logs are the real work, and the uninstall is a footnote.
  • Patching from a screenshot. Records get corrected and screenshots don’t, so follow it back to the maintainer’s own page before you act.

And one honest boundary. On a weekend project with no users and no secrets, most of this ceremony is overkill, so take the patch and move on. The discipline is for systems where being wrong costs something, which is exactly where you’ll be glad the assumption was written down.

What separates a senior from a junior here

  • A junior reads the score, a senior reads the assumptions behind it. The 5.3 versus 7.5 split is two teams answering “how hard is this to pull off” differently, and your own setup is what settles the argument.
  • A junior treats every alert the same way, a senior asks first whether it’s a mistake or an attack. An accidental hole gets the seven questions and a sensible date, while an attack that may already have run gets damage control, and mixing those up is expensive in both directions.
  • A junior closes an alert with “not affected”, a senior writes down what makes that true. The reason you wrote down survives the config change that would quietly undo it.
  • A junior updates now or never, a senior decides per update. Everyday releases wait a few days, reviewed security fixes go through a named exception, and both are written down rather than decided by mood.

When the next one lands

The two teams from step 4 both got it right, and they shipped on different days. Same package, same version, same advisory, two different answers, because their setups differed and each team could point at exactly where.

Being able to point at where is the whole trick, and it keeps until an alert actually turns up. Walk the seven questions, answer the ones you can and admit the ones you can’t, and put the result somewhere the next person will find it. If it comes out “not affected”, say what has to stay true for that to keep being right. Half an hour with the record and your own lockfile replaces an afternoon of dread, and the label on the next alert never gets the same grip on you again.

Want more of these in your Google results?