Back to glossary

Software Vulnerability

A software vulnerability is an unintended flaw in code or configuration that an attacker can exploit to break confidentiality, integrity, or availability.

Also known as Security vulnerability, Software vulnerabilities, Open source vulnerability

A software vulnerability, also called a security vulnerability, is an unintended flaw in code, configuration, or design that lets an attacker who controls some input break confidentiality, integrity, or availability. Vulnerabilities are mistakes, which separates them from malware: a vulnerability gets an advisory, a CVE, and a fixed version, while malware is the attack itself, fixed only by removal.

Three things must line up before a software vulnerability matters: a flaw in the code, a path for attacker-controlled input to reach it, and an impact the attacker wants. Advisories describe the first and assume the other two. That gap is why vulnerability counts mislead. On November 1, 2026, we resolved a fresh Create React App project with three declared dependencies. It pulled in 1,311 packages, and npm audit reported 73 vulnerabilities. Those 73 traced to 24 advisories in 11 packages. None of the 11 shipped in the production JavaScript bundle.

Our position: a vulnerability count is an inventory problem, not a to-do list. Shrink the inventory, fix at the root, and triage what remains by reachability and known exploitation, never by severity score alone. And a clean vulnerability scan does not mean a clean install. The axios compromise of March 2026 was malware, and malware reaches the advisory feed only after it has already run on someone's machine.

What is a software vulnerability?

#

A software vulnerability is one specific flaw in one specific product, exploitable by an attacker. MITRE's CWE program defines a vulnerability as "a specific instance of one or more weaknesses in a specific product that can be exploited," with a negative impact on confidentiality, integrity, or availability.

That definition separates four terms that get used interchangeably. A weakness is a category of mistake, catalogued by the Common Weakness Enumeration. A vulnerability is that mistake in a named package and version range, usually identified by a CVE and a GitHub Security Advisory (GHSA) ID. An exploit is the input or code that triggers the vulnerability. Malware is code written on purpose to cause harm, and needs no flaw at all.

TermWhat it isReal exampleIdentifier
WeaknessA category of mistakeInefficient regular expression complexityCWE-1333
VulnerabilityThat mistake in a specific product and version rangenth-check versions below 2.0.1CVE-2021-3803, GHSA-rp65-9cf3-cjxr
ExploitInput or code that triggers the vulnerabilityThe string "2n" followed by 80,000 spaces and "!"None
MalwareCode written to cause harm, no flaw requiredaxios@1.14.1 pulling in plain-crypto-js@4.2.1GHSA-fw8c-xr5c-95f9, no CVE

Software vulnerabilities live in two places. First-party vulnerabilities sit in code your team wrote. An insecure direct object reference (IDOR) is the classic case: GET /invoices/1042 returns any customer's invoice because the server checks that a session exists but not who owns the invoice. MITRE files that weakness as CWE-639, authorization bypass through a user-controlled key. Nobody publishes a CVE for a bug in your internal app, so you find it through code review, testing, and outside reports, never through an advisory feed.

Third-party vulnerabilities sit in dependencies, get public advisories, and are what software composition analysis matches against your lockfile. By volume, they dominate. Most of that code arrives as transitive dependencies that nobody on the team chose.

Anatomy of a real vulnerability: nth-check

#

nth-check parses CSS :nth-child() formulas such as 2n+1. Version 1.0.2, published in October 2018, does it with one regular expression:

JavaScript
// node_modules/nth-check/parse.js (nth-check@1.0.2)
var re_nthElement = /^([+\-]?\d*n)?\s*(?:([+\-]?)\s*(\d+))?$/;

The flaw is two adjacent \s* groups that can both match the same run of spaces. When the match fails at the end of the string, the regex engine retries every possible split of the spaces between the two groups. The work grows with the square of the input length. We measured it on Node.js 22.23.4:

JavaScript
// bench.js: node bench.js ./node_modules/nth-check
const nthCheck = require(process.argv[2]);
for (const n of [10000, 20000, 40000, 80000]) {
  const input = "2n" + " ".repeat(n) + "!";
  const t = process.hrtime.bigint();
  try { nthCheck(input); } catch (e) {}
  console.log(n, Number(process.hrtime.bigint() - t) / 1e6 | 0, "ms");
}
Spaces in inputnth-check 1.0.2nth-check 2.1.1
10,00040 msunder 1 ms
20,000162 msunder 1 ms
40,000673 msunder 1 ms
80,0002,590 msunder 1 ms

Doubling the input roughly quadruples the time. An 80 KB string blocks the Node.js event loop for 2.6 seconds, and a server doing nothing else stops answering requests for that long. The advisory calls this a regular expression denial of service and scores it 7.5, high, with the vector AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H. Version 2.0.1, released September 16, 2021, replaced the regex with a single-pass character scanner, and every run finishes in under a millisecond.

The CVSS vector is where the abstraction leaks. AV:N means "attackable over the network," which is true only if your code passes network input to nth-check. The CVSS v3.1 User Guide tells analysts scoring a library to "score for the reasonable worst-case implementation scenario." The score describes the worst plausible deployment. It cannot know yours. Some maintainers have given up on it entirely: in January 2025, the curl and Go security teams publicly rejected CVSS in favor of their own context-based severity calls.

In our Create React App project, nth-check 1.0.2 arrives five levels down: react-scripts → @svgr/webpack@5.5.0 → @svgr/plugin-svgo@5.5.0 → svgo@1.3.2 → css-select@2.1.0 → nth-check@1.0.2. svgo runs at build time on SVG files in your own repository. To exploit it, an attacker would need to commit a crafted SVG, and anyone who can commit to your repository has better options than slowing your build. We tried anyway: a crafted :nth-child() formula inside an SVG <style> block never reached nth-check, because svgo's CSS parser threw an error first. The vulnerability is real. In this deployment, we found no path from attacker-controlled input to it. A web service that optimizes user-uploaded SVGs with the same svgo version would be a different story.

That is the whole problem of vulnerability management in miniature. The advisory is correct, the score is correct, and the alert is still noise.

Why does one npm install report 73 vulnerabilities?

#

One npm install reports 73 vulnerabilities because npm audit counts every package that depends on a vulnerable package, not the vulnerabilities themselves. We created a project from the react-scripts 5.0.1 template with React 18.3.1, resolved it, and audited it on November 1, 2026 with npm 10.9.9:

JSON
{
  "name": "cra-demo",
  "private": true,
  "dependencies": {
    "react": "18.3.1",
    "react-dom": "18.3.1",
    "react-scripts": "5.0.1"
  }
}
Bash
$ npm install --package-lock-only --ignore-scripts
$ npm audit
...
nth-check  <2.0.1
Severity: high
Inefficient Regular Expression Complexity in nth-check - https://github.com/advisories/GHSA-rp65-9cf3-cjxr
fix available via `npm audit fix --force`
Will install react-scripts@0.0.0, which is a breaking change
node_modules/svgo/node_modules/nth-check
  css-select  <=3.1.0
  Depends on vulnerable versions of nth-check
  node_modules/svgo/node_modules/css-select
...
73 vulnerabilities (11 low, 49 moderate, 13 high)

One Create React App install, audited Nov 1, 2026: what npm audit counts vs. what ships

73
Packages flagged by the headline: 13 high, 49 moderate, 11 low
11
Packages with their own advisory. The other 62 only depend on one of them.
24
Distinct advisories in those 11 packages: 8 high, 13 moderate, 3 low
0
Vulnerable packages in the production bundle, per the source map. All 11 run only in the build, dev server, or test runner.
highmoderatelowBar length is relative to 73

The figure above narrows one number in four steps. npm reported 73 vulnerable packages: 11 low, 49 moderate, 13 high, 0 critical. Only 11 of those 73 packages have an advisory of their own. The other 62 are flagged because they depend on one of the 11. The 11 packages carry 24 distinct advisories: 8 high, 13 moderate, 3 low. We then built the app with react-scripts build and read the production source map. The shipped bundle contains react, react-dom, scheduler, and our own two files. It contains 0 of the 11 vulnerable packages.

StageCountWhat it measures
npm audit headline73Packages that are vulnerable or depend on something vulnerable
Packages with their own advisory11nth-check, postcss, svgo, serialize-javascript, webpack-dev-server, webpack-dev-middleware, underscore, uuid, minimatch, deepmerge, @tootallnate/once
Distinct advisories248 high, 13 moderate, 3 low, 0 critical
Vulnerable packages in the shipped bundle0Modules listed in the production source map

Two details in the output deserve attention. First, npm audit fix changed nothing. All 73 findings remained after we ran it, because a fresh resolve already picks the newest versions the ranges allow. Second, the only fix npm offers is react-scripts@0.0.0, the package's first upload, from July 2016. npm is proposing a downgrade across five major versions because no newer stable release exists. The project behind it has been officially deprecated since February 2025, and as of November 2026, 5.0.1, published April 12, 2022, is still its latest tag.

Dan Abramov, then on the React core team, made the same argument about this toolchain in 2021: "The way npm audit works is broken." One of his proposed fixes was to declare build tools as development dependencies and audit production only. We tested it. Moving react-scripts from dependencies to devDependencies and running npm audit --omit=dev reports 0 vulnerabilities. The same lockfile still reports 73 without the flag.

That one-line change is honest only if you understand what it hides. Abramov wrote in the same post that "development dependencies actually are an attack vector." Several of the 24 advisories describe real developer-machine risk. Three webpack-dev-server advisories, including GHSA-9jgg-88mc-972h, let a malicious website read your source code while the dev server runs. Excluding dev dependencies is the right filter for production exposure and the wrong filter for your laptop.

How is a software vulnerability different from malware?

#

A software vulnerability is an accident that might be exploited later. Malware is the exploitation, shipped in the package itself. The difference shows up most clearly when one package has both. As of November 1, 2026, npm's bulk advisory endpoint returns 35 advisories for axios@1.14.1:

Bash
$ curl -s -X POST https://registry.npmjs.org/-/npm/v1/security/advisories/bulk \
    -H 'content-type: application/json' \
    -d '{"axios":["1.14.1"],"nth-check":["1.0.2"]}' > bulk.json
$ jq '.axios | length' bulk.json
35
$ jq -c '.["nth-check"][0], (.axios[] | select(.cwe | index("CWE-506")))' bulk.json
JSON
{"id":1095141,"url":"https://github.com/advisories/GHSA-rp65-9cf3-cjxr","title":"Inefficient Regular Expression Complexity in nth-check","severity":"high","vulnerable_versions":"<2.0.1","cwe":["CWE-1333"],"cvss":{"score":7.5,"vectorString":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"}}
{"id":1115703,"url":"https://github.com/advisories/GHSA-fw8c-xr5c-95f9","title":"Malware in axios","severity":"critical","vulnerable_versions":"=1.14.1","cwe":["CWE-506"],"cvss":{"score":0,"vectorString":null}}

Thirty-four of those advisories are vulnerabilities that axios@1.14.0 shares: 12 high, 21 moderate, 1 low. Fourteen of the 34 are prototype pollution gadgets, which an attacker can use only after polluting Object.prototype through some other bug. The 35th is different in kind. GHSA-fw8c-xr5c-95f9 is tagged CWE-506, embedded malicious code, matches only the two malicious axios versions, and has no CVE. The advisory's remediation reads like an incident report, not a patch note: "Any computer that has this package installed or running should be considered fully compromised."

The two records have the same shape. Both have a severity, a CWE, a version range, and even a CVSS block, although the malware's score is 0 with no vector beside a severity of critical. That is why vulnerability scanners appear to cover malware. The timelines show why they do not.

Two advisories in the same npm feed, on two different clocks

Vulnerability: nth-check8+ years
  1. 2018-10-21nth-check 1.0.2 ships with the flawed regex
  2. 2021-09-162.0.1 replaces the regex
  3. 2021-09-20GHSA-rp65-9cf3-cjxr published, CVE-2021-3803
  4. 2026-11-01A fresh Create React App install still resolves 1.0.2
Malware: axios3 h 16 min
  1. 03-30 23:59 UTCplain-crypto-js@4.2.1 published with a postinstall dropper
  2. 03-31 00:05 UTCSocket flags it as malware
  3. 03-31 00:21 UTCaxios@1.14.1 published, depending on it
  4. 03-31 ~01:00 UTCaxios@0.30.4 published
  5. 03-31 03:15 UTCnpm removes both axios versions; GHSA-fw8c-xr5c-95f9 published that day, no CVE
TrackTimeEvent
Vulnerability2018-10-21nth-check 1.0.2 ships with the flawed regex
Vulnerability2021-09-16nth-check 2.0.1 replaces the regex
Vulnerability2021-09-20GHSA-rp65-9cf3-cjxr published, CVE-2021-3803
Vulnerability2026-11-01A fresh Create React App install still resolves nth-check 1.0.2, more than 8 years after it shipped
Malware2026-03-30 23:59 UTCplain-crypto-js@4.2.1 published with a postinstall dropper
Malware2026-03-31 00:05 UTCSocket flags plain-crypto-js as malware
Malware2026-03-31 00:21 UTCaxios@1.14.1 published, depending on plain-crypto-js@4.2.1
Malware2026-03-31 ~01:00 UTCaxios@0.30.4 published with the same dependency
Malware2026-03-31 03:15 UTCnpm removes both axios versions, 3 hours 16 minutes after the payload went live. GHSA-fw8c-xr5c-95f9 is published the same day, with no CVE

The nth-check vulnerability played out over eight years. The flawed version shipped on October 21, 2018. The fix shipped on September 16, 2021, and GitHub published the advisory on September 20, 2021. As of November 1, 2026, a fresh Create React App install still resolves nth-check 1.0.2. A scanner that checks advisories has years to find it.

The axios malware played out over about three hours. According to the maintainer's post-mortem, the malicious dependency plain-crypto-js@4.2.1 went live at 23:59 UTC on March 30, 2026, axios@1.14.1 followed at 00:21 UTC on March 31, and npm removed both axios versions at 03:15 UTC. Socket's scanner flagged plain-crypto-js at 00:05:41 UTC, sixteen minutes before the axios release that pulled it in. The payload ran in a postinstall hook, so it executed the moment npm finished installing the package. GitHub published the malware advisory the same day. An advisory-driven scanner can tell you afterward that you were compromised. It cannot stop the install.

Software vulnerabilityMalware
IntentAccidentalDeliberate
Typical CWEMany: CWE-1333, CWE-1321, CWE-918CWE-506
IdentifierCVE plus GHSAGHSA or OSV MAL- ID, rarely a CVE
Exposure windowMonths to years, until you upgradeOften hours for a hijacked popular package, until the registry removes it
FixUpgrade to a patched versionRemove it, then rotate every secret the machine held
Needs a triggerYes, attacker input must reach the flawNo, it runs at install or import
Detected byMatching versions against advisoriesAnalyzing package contents and behavior before install

Verdict: use advisory matching for vulnerabilities, because their windows are long and the version math works. For malware, use something that inspects package contents before install: a scanner that reads the code, an install-time firewall, or a dependency cooldown. Each has a cost. A 24-hour cooldown, which pnpm 11 now sets by default, would have skipped both malicious axios versions, but it delays a security fix by the same 24 hours. Content analysis can misfire on legitimate packages that do unusual things. And as of November 2026, npm v12 skips dependency install scripts by default, which would have stopped this postinstall payload but does nothing about malware that runs at import time.

How do you reduce a software vulnerability backlog?

#

You reduce a software vulnerability backlog by shrinking what you depend on and fixing at the root, before triaging anything. Triage is the expensive step, so make it run on the smallest possible set. In order:

1. Remove what you do not use, and declare what never ships. Every dependency you delete takes its whole subtree of advisories with it. Declaring build tools as devDependencies lets you audit production exposure separately, as the Create React App test showed. The cost: dev-only advisories still apply to developer machines and CI runners, so keep a second, separate queue for them.

2. Fix at the root, not the leaf. 62 of our 73 findings were inherited, and every one of them traced back to a single direct dependency. When the root is maintained, one upgrade closes dozens of findings. When the root is abandoned, as react-scripts is, the real fix is migration to a maintained tool. Dependency overrides can force a patched leaf version in the meantime. The cost: an override runs a combination of versions that nobody tested together, and it can break the build in ways the advisory never mentioned.

3. Triage by reachability, then by evidence of exploitation. Reachability analysis asks whether any path leads from your code into the vulnerable function. We have published that precomputed reachability, which reads only the dependency graph, marks about 60% of CVEs unreachable, and full application analysis typically filters about 80%. For what remains, known exploitation in CISA's KEV catalog overrides everything, and exploit probability orders the rest. Our vulnerability prioritization entry walks through that ordering. The cost: static reachability over-approximates by design. Dynamic imports and reflection produce "unknown" results that still need a human.

4. Record every decision once. A "not affected" verdict that lives in a ticket gets re-litigated on every scan. Write it down as a VEX statement next to the SBOM, so the next scan, auditor, and customer read the same answer. The cost: VEX statements go stale when your code changes, and nothing re-verifies them for you.

5. Gate new findings at the pull request. A backlog is a stock, and pull requests are the flow. Blocking a newly introduced reachable high-severity vulnerability usually costs one developer a few minutes. Discovering it six months later costs a triage cycle. The cost: gates that fire on unreachable findings train developers to click past them.

Sorting by CVSS is not on the list. In the axios example, 12 of the 34 vulnerability advisories score high, and 14 of the 34 need an unrelated bug to exist first. Severity cannot tell those apart. Reachability can.

Where software vulnerability data comes from, and why it is thinning

#

Software vulnerability data starts with a CVE Numbering Authority (CNA) assigning an ID. The National Vulnerability Database historically added the severity score and product matching that tools rely on. Package ecosystems lean more on GitHub's advisory database and OSV, which record the package name, affected range, and fixed version that npm audit and every other software composition analysis tool consume.

The CVE side is buckling under volume. 48,185 CVEs were published in 2025, 20.6% more than in 2024. In April 2026, NIST stopped enriching most CVEs and now prioritizes those in CISA's KEV catalog, software the federal government uses, and software designated critical under Executive Order 14028. NIST cited a 263% rise in CVE submissions between 2020 and 2025. As of August 17, 2026, the NVD had received 53,115 new CVEs that year and enriched 30,531, with 42,353 records marked "Not Scheduled".

Discovery is about to accelerate further. Anthropic reports that over six months its models found more than 29,000 candidate vulnerabilities in open source projects, of which humans reviewed about 6,000. curl's maintainer Daniel Stenberg reported in May 2026 that the project was receiving security reports at four to five times its 2024 rate. Every one of those findings has to be reproduced, fixed, released, and then installed by you.

The CVE and NVD pipeline cannot be a team's only source of truth anymore. Package-level advisories from GHSA and OSV usually arrive with the version ranges a scanner needs, and they keep arriving whether or not the NVD ever enriches the matching CVE.

The open problem is arithmetic. Discovery scales with compute, and remediation scales with maintainers and the people who upgrade. A team that measures itself by its open vulnerability count will lose that race every quarter. The number that matters is how many reachable, exploitable vulnerabilities sit in code you actually run. The packages most likely to hurt you will never appear in that number, because malware does not wait for an advisory.