Back to glossary

Common Vulnerability Scoring System (CVSS)

CVSS is FIRST's open standard that turns a vulnerability's attack conditions and impact into a 0.0 to 10.0 severity score. It measures severity, not risk.

Also known as Common Vulnerability Score, CVSS score, CVSS v4.0, CVSS v3.1

The Common Vulnerability Scoring System (CVSS) is FIRST's open standard for rating how severe a software vulnerability is, sometimes called the Common Vulnerability Score. An analyst encodes how the flaw is reached and what it breaks into a vector string, and a published formula turns that vector into a 0.0 to 10.0 score. CVSS measures worst-case severity, not your risk.

That last sentence is the one most teams get wrong. A CVSS score answers "how bad could this be, for a reasonable worst-case deployment?" It does not answer "will anyone exploit this?" or "does my code even call the vulnerable function?" FIRST, which maintains the standard, says so directly: "CVSS Base (CVSS-B) scores are designed to measure the severity of a vulnerability and should not be used alone to assess risk" (CVSS v4.0 user guide). FIRST's own data shows the size of the gap. Fixing every CVE scored 7.0 or higher means working on 57.4% of all CVEs, and only 3.96% of those show observed exploitation (FIRST EPSS model). Our position is that CVSS should set how fast you respond once you know a vulnerability matters. Exploitation evidence and reachability should decide whether it matters at all.

How does CVSS turn a vector string into a score?

#

CVSS turns a vector string into a score by mapping each metric value to a fixed weight and combining the weights with a published equation. Here is a real one. We installed minimist@1.2.5 into an empty project and ran npm audit --json (npm 10.9.9, November 5, 2026):

JSON
{
  "name": "minimist",
  "severity": "critical",
  "via": [{
    "title": "Prototype Pollution in minimist",
    "url": "https://github.com/advisories/GHSA-xvch-5gv4-984h",
    "cwe": ["CWE-1321"],
    "cvss": {
      "score": 9.8,
      "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
    },
    "range": ">=1.0.0 <1.2.6"
  }]
}

The vector string is the actual data. The 9.8 is derived from it. Each key:value pair is one metric:

  • AV:N Attack Vector is Network: the attacker can reach the flaw remotely.
  • AC:L Attack Complexity is Low: no special conditions are needed.
  • PR:N Privileges Required is None.
  • UI:N User Interaction is None.
  • S:U Scope is Unchanged: the impact stays inside the vulnerable component.
  • C:H/I:H/A:H Confidentiality, Integrity, and Availability impact are all High.

The CVSS v3.1 specification assigns each value a weight and combines them in two halves. The impact half starts from an Impact Sub-Score, ISS = 1 − (1−C)(1−I)(1−A), then scales it by 6.42 when scope is unchanged. The exploitability half is 8.22 × AV × AC × PR × UI. The base score is the sum, capped at 10 and rounded up to one decimal.

For the minimist vector, every input is at or near its maximum. High impact on all three properties gives an ISS of 0.9148 and an impact of 5.8731. Network, low complexity, no privileges, and no interaction carry weights of 0.85, 0.77, 0.85, and 0.85, which give an exploitability of 3.8870. Each High impact carries a weight of 0.56. The sum is 9.7602, which rounds up to 9.8, Critical.

You can reproduce every number in this article. The weights come straight from section 7.4 of the specification:

Python
import math

W = {"AV": {"N": .85, "A": .62, "L": .55, "P": .2}, "AC": {"L": .77, "H": .44},
     "UI": {"N": .85, "R": .62}, "CIA": {"H": .56, "L": .22, "N": 0}}
PR = {"U": {"N": .85, "L": .62, "H": .27}, "C": {"N": .85, "L": .68, "H": .5}}

def roundup(x):  # spec Appendix A: avoids floating-point drift
    i = round(x * 100000)
    return i / 100000 if i % 10000 == 0 else (math.floor(i / 10000) + 1) / 10

def base(vector):
    m = dict(p.split(":") for p in vector.split("/")[1:])
    iss = 1 - (1 - W["CIA"][m["C"]]) * (1 - W["CIA"][m["I"]]) * (1 - W["CIA"][m["A"]])
    changed = m["S"] == "C"
    impact = 7.52 * (iss - .029) - 3.25 * (iss - .02) ** 15 if changed else 6.42 * iss
    expl = 8.22 * W["AV"][m["AV"]] * W["AC"][m["AC"]] * PR[m["S"]][m["PR"]] * W["UI"][m["UI"]]
    if impact <= 0:
        return 0.0
    return roundup(min((1.08 if changed else 1) * (impact + expl), 10))

print(base("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"))  # 9.8  minimist
print(base("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H"))  # 10.0 Log4Shell

Changing one letter moves the score a long way. Flip S:U to S:C, meaning the exploit escapes the vulnerable component, and the same vector becomes 10.0. That is the score the NVD assigned to Log4Shell, CVE-2021-44228. The score then maps onto five qualitative bands: None (0.0), Low (0.1 to 3.9), Medium (4.0 to 6.9), High (7.0 to 8.9), and Critical (9.0 to 10.0). Those labels are what npm audit, the GitHub Advisory Database, and most scanners print.

Why does minimist score 9.8?

#

minimist scores 9.8 because CVSS tells library analysts to assume the worst plausible caller. minimist parses command-line arguments. To pollute Object.prototype through it, an attacker has to control the strings passed to the parser. In most programs those come from whoever typed the command. The vector still says AV:N, network-reachable, with no privileges and no user interaction.

That is not a scoring mistake. It is the rule. The CVSS v3.1 user guide is explicit about libraries: "The analyst should score for the reasonable worst-case implementation scenario." A library has no single deployment, so its score describes the most exposed program anyone might build with it. For open source dependencies, this one rule explains most of the distance between a scanner's severity column and what a team actually needs to fix.

CVSS v4.0 replaced the equation with expert-ranked lookup tables

#

CVSS v4.0, published by FIRST on November 1, 2023, dropped the weighted equation entirely. Its specification describes how the working group grouped about 15 million possible vectors into 270 equivalence sets. Experts ranked representative vectors from each set against each other, and that ranking became a lookup table. A vector's score is its set's table value, adjusted by interpolation toward neighboring sets and rounded to one decimal.

The metric model changed too. Scope is gone, replaced by separate impact metrics for the vulnerable system (VC/VI/VA) and subsequent systems (SC/SI/SA). A new Attack Requirements (AT) metric captures preconditions such as a race window. User Interaction now distinguishes Passive from Active. The Temporal group became the Threat group, and an optional Supplemental group carries non-scoring context such as Automatable and Safety.

v4.0 also forces you to say which metric groups were used. CVSS-B is Base only, CVSS-BT adds Threat, CVSS-BE adds Environmental, and CVSS-BTE has all three. The Threat group holds one metric, Exploit Maturity (E), and the spec defines its unset value as equivalent to Attacked. In other words, a Base-only v4.0 score assumes active exploitation.

That default is easy to see in the numbers. We scored Log4Shell-shaped vectors with the cvss Python package (version 3.6), which implements the v4.0 reference lookup:

CVSS v4.0 vector, after the shared prefix CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:HScore
SC:H/SI:H/SA:H (Base only, so E defaults to Attacked)10.0 Critical
SC:H/SI:H/SA:H/E:P (public proof of concept)9.3 Critical
SC:H/SI:H/SA:H/E:U (no known exploit)9.1 Critical
SC:N/SI:N/SA:N (Base only)9.3 Critical
SC:N/SI:N/SA:N/E:U (no known exploit)8.1 High

Adding threat information can only lower a v4.0 score, never raise it. The worst-case assumption is now written into the spec instead of hidden in practice. When you consume a v4.0 score, check whether it carries an E value. If it does not, it already assumes the bug is under attack.

CVSS v3.1CVSS v4.0
Score computationWeighted equationExpert-ranked lookup table plus interpolation
Cross-component impactS:U or S:C (Scope)Separate VC/VI/VA and SC/SI/SA metrics
PreconditionsFolded into ACSeparate AT (Attack Requirements)
Time-varying metricsTemporal: E, RL, RCThreat: E only
Unset exploit maturityTreated as worst caseTreated as Attacked (worst case)
Labeling"CVSS score"CVSS-B, -BT, -BE, -BTE
VerdictStill what every advisory in this article carries; fine to consumeBetter model; prefer it when a source offers both, and always check which groups were scored

Why does the same CVE get different CVSS scores?

#

The same CVE gets different CVSS scores because several organizations score it independently, and CVSS leaves room for judgment. The CVE Numbering Authority (CNA) that assigns the CVE may include a score. The NVD has historically added its own. CISA adds scores through its Vulnrichment program as an Authorized Data Publisher (ADP). The GitHub Advisory Database scores the advisories it reviews.

Those scores disagree more than most dashboards admit. Jerry Gamblin's Consensus Engine compared the two sources for 1,567 CVEs that both NVD and GitHub had scored. NVD came in higher 885 times, and GitHub came in higher 682 times. The largest single gap was 6.9 points. Gaps of 4 points or more appeared repeatedly, which is the difference between Medium and Critical for the same bug.

The curl project's CVE-2024-11053 shows how an ADP score can override the people who know the code. curl rated the netrc credential leak Low. Through its Vulnrichment program, CISA published a 9.1 Critical. After curl's maintainer, Daniel Stenberg, objected, CISA revised it to 3.4. We read the current record from the CVE.org API on November 5, 2026:

JSON
{
  "cna": { "providerMetadata": { "shortName": "curl" },
           "title": "netrc and redirect credential leak" },
  "adp": [{
    "providerMetadata": { "shortName": "CISA-ADP" },
    "metrics": [{ "cvssV3_1": {
      "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N",
      "baseScore": 3.4,
      "baseSeverity": "LOW"
    }}]
  }]
}

Run that vector through cvss31.py and you get 3.4. A 9.1 under v3.1 needs something like AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N. The disagreement was never about arithmetic. It was about which letters an analyst picked. The incident prompted Stenberg's "CVSS is dead to us" post, and the Go security team has also declined to use CVSS. curl's advisory still says "Severity: Low," and the project no longer puts CVSS on its own CVE records.

The pipeline that supplies most CVSS scores is also thinning out. The NVD halted most enrichment in February 2024. In April 2026, NIST stopped enriching most CVEs. As of November 2026, the NVD prioritizes CVEs in CISA's KEV catalog, software the federal government uses, and software designated critical. NIST also said the NVD would stop producing its own CVSS score when the CNA already supplied one. NIST attributed the change to CVE submissions rising 263% between 2020 and 2025. Meanwhile 2025 set a record of 48,185 CVEs, with an average CVSS of 6.60. Any process that treats "the CVSS score" as a single authoritative number is reading one of several opinions, and increasingly the vendor's own.

What is the difference between CVSS and EPSS?

#

CVSS measures how bad a vulnerability would be if exploited, and the Exploit Prediction Scoring System (EPSS) estimates how likely exploitation is. FIRST maintains both. EPSS is a machine-learning model that publishes a daily probability, from 0 to 1, that exploitation activity will be observed against a CVE in the next 30 days. It is trained on exploitation telemetry from honeypots and intrusion sensors, not on analyst judgment.

The two disagree constantly, and both of our running examples show it. On November 5, 2026, the FIRST EPSS API returned 0.99999 for Log4Shell and 0.04581 for minimist's CVE-2021-44906. Both are CVSS Critical. One is near-certain to be exploited in any 30-day window. The other sits around a 4.6% chance.

FIRST measured what each approach buys on October 2023 data covering 139,473 CVEs. The rule "remediate everything CVSS 7 or above" and the rule "remediate everything EPSS 0.1 or above" produce very different workloads:

StrategyShare of all CVEs you work on (effort)Share of exploited CVEs you catch (coverage)Share of your work that was exploited (efficiency)
CVSS 7.0 and above57.4%82.2%3.96%
EPSS 0.1 and above2.7%63.2%65.2%

The CVSS rule catches more exploited CVEs, at roughly 21 times the effort, and about 96% of that effort goes to CVEs with no observed exploitation. FIRST notes the 0.1 threshold is arbitrary, not a recommendation. Neither rule is a complete strategy. They answer different questions. The right move is to combine them with CISA's KEV list, a decision framework such as SSVC, and signals about your own code.

SignalQuestion it answersSourceBlind spot
CVSSHow bad, in the worst plausible deployment?CNA, NVD, CISA ADP, GHSA analystsIgnores likelihood and your deployment
EPSSHow likely is exploitation in the next 30 days?FIRST model, dailySays nothing about impact or about your code
CISA KEVHas this been exploited in the wild?CISAConservative and lagging; absence proves nothing
SSVCWhat should this organization do: track, attend, or act?Decision tree run by each organizationOnly as good as the inputs you feed it
ReachabilityCan my code reach the vulnerable function?Static analysis of your dependency graph and codeSays nothing about attacker interest

Verdict: use KEV and EPSS to decide what is urgent, reachability to decide what is relevant, and CVSS to set the deadline once both say yes. Use CVSS alone only when a compliance framework requires it, and record the vector, not just the number.

CVSS has nothing to say about malicious packages

#

CVSS scores vulnerabilities: mistakes in otherwise legitimate code. A malicious package is not a mistake. A typosquat or a hijacked release that runs a credential stealer in a postinstall script has no vulnerable function, no affected-but-patched range, and usually no CVE. The npm security team and the OpenSSF Malicious Packages project report these in OSV format, and GitHub publishes them as a separate class of malware advisories.

A pipeline that gates on "no Critical or High CVSS" therefore passes most supply chain attacks without comment. That is why, at Socket, malware is a different alert class from vulnerabilities. Our CVE alerts come in four severities (Critical CVE, High CVE, Medium CVE, Low CVE). "Known malware" is a separate alert. It fires when Socket's scanner, our threat researchers, or an external database flag the package version as malicious, and no severity score is involved.

Severity should set the clock, not pick the work

#

The workable use of CVSS is narrow. Use the band to set response time limits for vulnerabilities you have already decided are real. Do not use it to decide which of 2,000 findings deserve a ticket. Our guide to building a vulnerability management program starts with CVSS-tiered SLAs, such as 7 days for Critical, because they are simple and auditors understand them. It also says what breaks first as volume grows. Developers hit vulnerability fatigue, SLAs slip, and automated prioritization becomes essential.

For open source dependencies, that prioritization is reachability analysis. Most CVEs in a transitive dependency sit in code your application never executes. Socket's precomputed reachability works from manifests alone and typically marks about 60% of CVEs unreachable. Full application reachability analyzes your code too and typically filters about 80%. By default, Socket downgrades unreachable CVEs to a "monitor" action instead of "block" or "warn," so a 9.8 that your code cannot reach stops failing builds. The cost is that reachability is a static approximation. It cannot tell you whether attackers care, and paths the analysis cannot resolve come back "unknown" or "maybe reachable" rather than cleared.

The harder problem is upstream of any tool. As Stenberg put it, CVSS works when you know exactly how the product is used. Open source is the opposite case. One library ships into millions of deployments, the rules require scoring the worst one, and the organizations that used to provide an independent second opinion are pulling back. A CVSS score on an open source package is best read as an upper bound written by someone who has never seen your code. Treat it that way, keep the vector next to the number, and let evidence about exploitation and reachability move it down.