Web Security (OWASP Top 10:2025) Module 4 – A03:2025 Software Supply Chain Failures
Know your dependencies: inventory, SBOMs, audits
Build an inventory and an SBOM from a lockfile, match advisories by their version ranges and set fix deadlines by risk, on a sample app of 91 package versions.
What you will learn
- Explain why A03:2025 widened vulnerable components into software supply chain failures
- Generate a direct and transitive dependency inventory and a CycloneDX SBOM from a lockfile
- Decide whether an advisory's version ranges cover an installed version
- Prioritise findings by exploitation, exposure and reach, with deadlines such as CERT-In's indicative timelines
- Choose whether to refresh, raise, override, upgrade, replace or remove a vulnerable dependency
Before you start
What you’ll learn · Before you start
What you will learn
- Explain why A03:2025 widened vulnerable components into software supply chain failures
- Generate a direct and transitive dependency inventory and a CycloneDX SBOM from a lockfile
- Decide whether an advisory's version ranges cover an installed version
- Prioritise findings by exploitation, exposure and reach, with deadlines such as CERT-In's indicative timelines
- Choose whether to refresh, raise, override, upgrade, replace or remove a vulnerable dependency
Before you start
On this page
ShopNest’s package.json asks for four packages. Its lockfile installs 91 distinct package versions, and 87 of them are there only because another package needs them. Check that list against nine advisories from the OSV database and six findings come back, three of them in packages that nobody on the team chose.
You will build that inventory from the lockfile, turn it into a CycloneDX SBOM with npm’s own command, decide which versions an advisory’s ranges really cover, and turn the findings into deadlines and fixes. Every number about ShopNest on this page comes from programs that ran on Node.js 24.21.0 and Python 3.14.8.
A03 now covers everything your software is built from
The OWASP Top 10 first warned about this area in 2013, as Using Components with Known Vulnerabilities, and the 2021 edition called it Vulnerable and Outdated Components. In 2025 it became A03, Software Supply Chain Failures: failures or compromises anywhere in the chain that builds, ships or updates your software, whether the cause is a flawed library, a hostile one or a weak build system.
Two numbers on OWASP’s page explain the change. Exactly half of the respondents to its community survey ranked this category first, yet only 11 CVE: Common Vulnerabilities and Exposures, the program that identifies, defines and catalogs publicly disclosed vulnerabilities. A CVE ID names one flaw in one product, with the versions it affects. More in “Reading the OWASP Top 10:2025, CWE and ASVS” records map to its six CWE: Common Weakness Enumeration, MITRE's community-developed list of software and hardware weakness types. A CWE ID such as CWE-639 names a kind of mistake that can appear in any product. More in “Reading the OWASP Top 10:2025, CWE and ASVS” entries, so testing data alone would have ranked it low: most supply chain failures never become a CVE in your own code. The CWEs name the patterns this module deals with, among them CWE-1104, a third-party component that is no longer maintained, CWE-1329, one that cannot be updated, CWE-1357, one that is not trustworthy enough, and CWE-1395, a dependency on a component with a known vulnerability. Step through the parts ShopNest is built from, and where the failures in OWASP’s examples came in:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- The code you can see. ShopNest's own code is the part the team writes and reviews. It is also the smallest part of what runs in production.
- Packages you chose. package.json names 4 packages. Each one runs inside the app with the app's own rights, so a flaw in any of them is a flaw in ShopNest.
- Their dependencies. Those 4 bring in 87 more versions that nobody on the team picked. One of OWASP's examples of a flawed component is Log4Shell, remote code execution in the Apache Log4j logging library, running with each application's privileges.
- A release that is malicious. Some packages are not flawed but hostile. In 2025 the Shai-Hulud worm spread through npm in an install script that stole tokens and used them to publish infected versions of other packages, more than 500 package versions in all.
- The tools around the code. Editors, their extensions and command-line tools run with a developer's rights and tokens. OWASP asks for changes to them to be tracked like changes to the code.
- The pipeline that builds it. The CI pipeline holds the secrets that publish and deploy. OWASP warns that it is often weaker than the systems it builds; lesson 3 of this module hardens it.
- Updates you install. Vendor software arrives as updates. In the 2019 SolarWinds compromise a trusted vendor's update carried malware to about 18,000 organisations, which is why OWASP advises staged rollouts.
- What this lesson covers. This lesson inventories the packages and checks them against advisories. The next lesson keeps hostile packages out, and the last one protects the pipeline and the artifact.
Not affiliated
MySmartCoPilot is independent of OWASP, CERT-In, CISA, npm and the maintainers of every package this lesson names. The incidents come from OWASP’s Top 10:2025 and the findings from the OSV database, both under References; they show how failures arrive, not a fault of any project.
The verification standard turns the category into requirements you can check. In ASVS: OWASP's Application Security Verification Standard. Version 5.0.0 has 345 requirements in 17 chapters and three cumulative levels; a requirement is cited with its version, as in v5.0.0-8.2.2. More in “Reading the OWASP Top 10:2025, CWE and ASVS” 5.0.0, requirement 15.1.1 asks for documented, risk-based time frames for updating third-party components, 15.1.2 for an inventory such as an SBOM of every third-party library, 15.2.1 that no component has outlived its documented window, and 15.2.4 that every transitive dependency comes from the repository you expect. This lesson meets the first three; the next lesson deals with the fourth.
Checkpoint
Check: What A03:2025 covers, and why
The lockfile is the inventory, and most of it is transitive
package.json says what you asked for, often as a range: ShopNest asks for express ^5.1.0, which means any 5.x
release from 5.1.0 on. The lockfile: A file the package manager writes, such as package-lock.json, that records the exact version, source and integrity hash of every installed package, so that every install of the project gets the same tree., package-lock.json, says what you got: every installed
package with its exact version, the address it came from and an integrity hash of the file. npm ci installs exactly
that tree and stops with an error when the lockfile and package.json disagree, so commit the lockfile and build with
npm ci: the tree you scan is then the tree you ship.
This program reads ShopNest’s two files and follows every dependency the way Node finds it, from the nearest node_modules folder upwards:
// ShopNest's inventory from its own files: what package.json asks for, and what package-lock.json says is installed.
// The lockfile (version 3) lists every installed package under its folder ("node_modules/express"); this program
// follows each package's dependencies the way Node finds them, nearest node_modules folder first.
import { readFileSync } from 'node:fs';
const manifest = JSON.parse(readFileSync('shopnest/package.json', 'utf8'));
const { packages } = JSON.parse(readFileSync('shopnest/package-lock.json', 'utf8'));
const folders = Object.keys(packages).filter((path) => path !== '');
const nameOf = (path) => path.slice(path.lastIndexOf('node_modules/') + 'node_modules/'.length);
const idOf = (path) => `${nameOf(path)}@${packages[path].version}`;
const distinct = (paths) => new Set(paths.map(idOf));
/** The folder a dependency of the package at `from` resolves to: its own node_modules, then each one above it. */
function resolve(from, name) {
for (let base = from; ; ) {
const path = `${base ? `${base}/` : ''}node_modules/${name}`;
if (packages[path]) return path;
if (!base) return null;
const cut = base.lastIndexOf('/node_modules/');
base = cut < 0 ? '' : base.slice(0, cut);
}
}
const needs = (from) => {
const e = packages[from];
return Object.keys({ ...e.dependencies, ...e.optionalDependencies, ...e.peerDependencies }).map((n) => resolve(from, n)).filter(Boolean);
};
/** Every folder reachable from `start`, with the shortest chain that reaches it. */
function reach(start) {
const chain = new Map([[start, [start]]]);
for (const path of chain.keys()) for (const next of needs(path)) if (!chain.has(next)) chain.set(next, [...chain.get(path), next]);
return chain;
}
const direct = Object.keys(manifest.dependencies);
const versions = distinct(folders);
const names = new Set(folders.map(nameOf));
console.log(`package.json asks for ${direct.length} packages. The lockfile installs ${folders.length} folders:`);
console.log(`${versions.size} distinct versions of ${names.size} packages.`);
for (const name of direct) console.log(` ${name.padEnd(8)} asks ${manifest.dependencies[name].padEnd(14)} locked ${packages[`node_modules/${name}`].version}`);
const transitive = versions.size - direct.length;
console.log(`Transitive: ${transitive} of the ${versions.size} versions (${((100 * transitive) / versions.size).toFixed(1)} %)`);
const twice = [...names].filter((n) => new Set(folders.filter((p) => nameOf(p) === n).map((p) => packages[p].version)).size > 1);
console.log(`In two versions at once: ${twice.join(', ')}`);
const through = Object.fromEntries(direct.map((n) => [n, distinct([...reach(`node_modules/${n}`).keys()].slice(1))]));
console.log(`Versions each one brings in: ${direct.map((n) => `${n} ${through[n].size}`).join(', ')}`);
const shared = [...through.express].filter((id) => through.multer.has(id));
console.log(`Brought in by both express and multer: ${shared.length} (${shared.join(', ')})`);
const all = reach('');
const deepest = [...all.values()].reduce((a, b) => (b.length > a.length ? b : a));
console.log(`Deepest chain, ${deepest.length - 1} levels: shopnest > ${deepest.slice(1).map(nameOf).join(' > ')}`);
console.log(`No licence recorded: ${[...distinct(folders.filter((p) => !packages[p].license))].join(', ') || 'none'}`);
console.log(`Install scripts: ${[...distinct(folders.filter((p) => packages[p].hasInstallScript))].join(', ') || 'none'}`);
for (const p of folders.filter((path) => packages[path].deprecated)) console.log(`Deprecated: ${idOf(p)}, "${packages[p].deprecated}"`); Output
package.json asks for 4 packages. The lockfile installs 92 folders: 91 distinct versions of 85 packages. express asks ^5.1.0 locked 5.1.0 helmet asks ^8.1.0 locked 8.1.0 lodash asks 4.17.20 locked 4.17.20 multer asks ^1.4.5-lts.2 locked 1.4.5-lts.2 Transitive: 87 of the 91 versions (95.6 %) In two versions at once: content-type, iconv-lite, media-typer, mime-db, mime-types, type-is Versions each one brings in: express 66, helmet 0, lodash 0, multer 22 Brought in by both express and multer: 1 (inherits@2.0.4) Deepest chain, 7 levels: shopnest > express > qs > side-channel > side-channel-map > get-intrinsic > get-proto > dunder-proto No licence recorded: busboy@1.6.0, streamsearch@1.1.0 Install scripts: none Deprecated: multer@1.4.5-lts.2, "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version."
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node inventory.mjs
Four requests became 91 distinct versions in 92 folders. content-type 2.1.0 sits in two folders, and six packages are installed in two versions at once, because the packages that need them ask for ranges no single version satisfies. 87 of the 91 versions, 95.6 %, are transitive dependency: A package your project does not name itself but that one of its dependencies, or theirs, needs. A lockfile records each one with its exact version; a scan that reads only the project's manifest misses them.: nobody chose them and package.json never mentions them. The deepest chain runs seven levels below the app, from express through qs to dunder-proto.
The lockfile carries warnings as well. multer 1.4.5-lts.2 is marked deprecated, with its maintainers’ message that 1.x
is affected by vulnerabilities fixed in 2.x. busboy and streamsearch have no licence recorded, because their
package.json files use an older licenses list, which npm’s documentation calls not valid metadata: a tool that reads
only the license field reports nothing for them, although both list MIT in the older form.
Scanning package.json instead of the lockfile
package.json holds 4 names and their ranges, not versions. The 87 transitive versions, where three of this lesson’s six findings sit, appear only in the lockfile. Scan the lockfile, or an SBOM made from it.
Checkpoint
Check: The lockfile as the inventory
An SBOM is that inventory in a format other tools can read
A SBOM (software bill of materials): A machine-readable inventory of the components in a piece of software: each name, exact version, identifier and hash, and how they depend on each other, written in a standard format such as CycloneDX or SPDX. is the same inventory written in a standard format, so that
scanners, customers and your own monitoring can all read it. Two formats lead. CycloneDX, an OWASP project, is at
version 1.7, which Ecma International publishes as the second edition of ECMA-424. SPDX, a Linux Foundation project
published as the international standard ISO/IEC 5962:2021, has since reached version 3.0.1. Both name each component
by its package URL (purl): A standard identifier for a package in any ecosystem, such as pkg:npm/express@5.1.0: the type, an optional namespace, the name and the version, with optional qualifiers. Ecma standardised it as ECMA-427., such as pkg:npm/express@5.1.0, an identifier Ecma standardised as
ECMA-427, so that every tool recognises the same package.
npm writes SBOMs itself. This program runs npm sbom on ShopNest’s lockfile alone, with no network, then checks the
result against twelve of the 21 data fields that CERT-In’s SBOM guidelines list as minimum elements:
// npm's own SBOM command, run on ShopNest's lockfile alone (--package-lock-only, --offline: no network), then the
// SBOM it wrote checked against twelve of the data fields CERT-In's SBOM guidelines list as minimum elements.
import { spawnSync } from 'node:child_process';
const npm = (...args) => spawnSync('npm', [...args, '--offline', '--cache=./npm-cache'], { cwd: 'shopnest', encoding: 'utf8' });
const run = npm('sbom', '--sbom-format=cyclonedx', '--sbom-type=application', '--package-lock-only');
if (run.status !== 0) throw new Error(run.stderr);
const bom = JSON.parse(run.stdout);
const parts = bom.components;
const tool = bom.metadata.tools[0];
console.log(`npm ${tool.version} wrote a ${bom.bomFormat} ${bom.specVersion} SBOM of ${run.stdout.length.toLocaleString('en')} characters.`);
console.log(`It describes ${bom.metadata.component.purl} (${bom.metadata.component.type}) and ${parts.length} components.`);
for (const name of ['express', 'multer', 'busboy']) {
const c = parts.find((x) => x.name === name);
const licence = c.licenses?.map((l) => l.license.id).join(' ') ?? 'no licence';
console.log(` ${c.purl.padEnd(28)} ${licence.padEnd(11)} ${c.hashes[0].alg} ${c.hashes[0].content.slice(0, 12)}…`);
}
const count = (test) => `${parts.filter(test).length} of ${parts.length}`;
const root = bom.dependencies.find((d) => d.ref === bom.metadata.component['bom-ref']);
const fields = [
['Component name and version', count((c) => c.name && c.version)],
['Unique identifier (purl)', count((c) => c.purl)],
['Checksums or hashes', count((c) => c.hashes?.length)],
['Component licence', count((c) => c.licenses?.length)],
['Component supplier', count((c) => c.supplier)],
['Component description', count((c) => c.description)],
['Component dependencies', `${bom.dependencies.length} entries; the app's own lists ${root.dependsOn.length}`],
['Author of the SBOM data', bom.metadata.tools.length ? `the tool (npm ${tool.version}), no person or organisation` : 'none'],
['Timestamp', bom.metadata.timestamp ? 'present' : 'none'],
['Release date, end-of-life date', count((c) => c.properties?.some((p) => /date|eol/i.test(p.name)))],
];
console.log('\nCERT-In minimum data field In this SBOM');
for (const [field, found] of fields) console.log(` ${field.padEnd(32)} ${found}`); Output
npm 11.19.0 wrote a CycloneDX 1.5 SBOM of 77,891 characters. It describes pkg:npm/shopnest@1.4.0 (application) and 91 components. pkg:npm/express@5.1.0 MIT SHA-512 0d3f5c939608… pkg:npm/multer@1.4.5-lts.2 MIT SHA-512 5731a256281c… pkg:npm/busboy@1.6.0 no licence SHA-512 f121506e0ff4… CERT-In minimum data field In this SBOM Component name and version 91 of 91 Unique identifier (purl) 91 of 91 Checksums or hashes 91 of 91 Component licence 89 of 91 Component supplier 0 of 91 Component description 0 of 91 Component dependencies 92 entries; the app's own lists 4 Author of the SBOM data the tool (npm 11.19.0), no person or organisation Timestamp present Release date, end-of-life date 0 of 91
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node make_sbom.mjs
The SBOM has 91 components, one per distinct version, each with its package URL and a SHA-512 hash taken from the lockfile’s integrity value. It is a good start and not a complete record: no component names its supplier, none has a description, because the lockfile holds none, busboy and streamsearch have no licence, and the only author is the tool. Fill such gaps before an SBOM goes to a customer, and keep the internal one complete.
npm 11.19.0 writes CycloneDX 1.5
The run shows npm 11.19.0, the version that comes with Node.js 24.21.0, writing specVersion 1.5, while the current specification is 1.7. Check which versions the tools that read your SBOMs accept before you choose a generator.
An SBOM also depends on when it was made. CERT-In’s guidelines describe six kinds, one for each stage of the life cycle. ShopNest’s is a Source SBOM, made from the repository, and it cannot see what a container image adds later:
- Design
The planned components
Written before the code exists, it lists the components a design intends to use. It helps a team or a buyer judge a plan, but nothing in it has been built yet.
- Source
The development environment this lesson
Made from the source tree, its manifests and lockfiles. ShopNest's SBOM in this lesson is of this kind, generated from package-lock.json without installing anything.
- Build
What the build pulled in
Generated by the build itself, it records the source files, the resolved dependencies and the pre-built components that went into this build, including anything a build step downloaded.
- Analyzed
What the artifact contains
Made by inspecting the finished artifact, such as a container image, after the build. It finds what the image really holds, the operating system packages too, whatever the manifests said.
- Deployed
What one system runs
An inventory of the software installed and configured on a particular system, combining the other kinds with what the deployment added.
- Runtime
What is loaded while it runs
Made by watching the running software, it records the components it actually loads and talks to, including those loaded only at run time.
The timeline has 6 events, from Design to Runtime.
CERT-In's SBOM guidelines
India’s national CERT publishes Technical Guidelines on SBOM, QBOM and CBOM, AIBOM and HBOM. Version 2.0 lists 21 minimum data fields, advises keeping a complete SBOM for internal use and sharing a customised one with consumers, and describes VEX documents that tell those consumers which vulnerabilities affect a product.
Checkpoint
Check: SBOM formats, fields and kinds
An advisory affects a version only inside one of its ranges
An advisory rarely lists every affected version; it lists ranges. In the format of the OSV database, which also
carries the GitHub Advisory Database’s records, each range is a list of events in version order: introduced is the
first affected version, fixed the first version without the flaw and last_affected the last one known to have it,
with "0" standing for a version before all others. A version is affected when it falls in any one range, and
introduced and last_affected include their own version while fixed does not. A flaw often has one range per release
line, because maintainers fix each line separately: GHSA-9wv6-86v2-598j in path-to-regexp has five.
This program reads nine of those records, trimmed to the fields a matcher needs, and checks all 91 of ShopNest’s
versions against them. It first shows why versions must be compared by SemVer’s rules rather than as text. Press
Edit, add a pair such as ("1.10.0", "1.9.9") to the first list and run it again:
Python · ranges/match.py
# Which of ShopNest's installed versions do these advisories affect? advisories.json holds nine records of the OSV
# database, trimmed to the fields a matcher reads; installed.txt lists ShopNest's 91 versions, one name@version a line.
import json
def key(version):
"""A sort key in SemVer 2.0.0 order: parts compare as numbers, and 2.0.0-rc.1 comes before 2.0.0."""
core, _, pre = version.split("+")[0].partition("-")
numbers = tuple(int(part) for part in core.split("."))
if not pre:
return numbers + ((1,),)
labels = tuple((0, int(p)) if p.isdigit() else (1, p) for p in pre.split("."))
return numbers + ((0, *labels),)
def in_range(version, events):
"""OSV's rule: walk the events in version order; introduced and last_affected count the version itself,
fixed does not. "0" stands for a version before every other one."""
v, hit = key(version), False
for event in sorted(events, key=lambda e: () if e.get("introduced") == "0" else key(next(iter(e.values())))):
kind, bound = next(iter(event.items()))
if kind == "introduced" and (bound == "0" or key(bound) <= v):
hit = True
elif kind == "fixed" and key(bound) <= v:
hit = False
elif kind == "last_affected" and key(bound) < v:
hit = False
return hit
print("Comparing versions as text instead of by SemVer gets some pairs wrong:")
for low, high in [("6.9.7", "6.14.1"), ("8.10.0", "8.4.0"), ("2.0.0-rc.1", "2.0.0"), ("1.4.4-lts.1", "1.4.5-lts.2")]:
semver, text = key(low) < key(high), low < high
wrong = " wrong" if semver != text else ""
print(f" is {low:<11} below {high:<11} SemVer: {semver!s:<5} text: {text!s:<5}{wrong}".rstrip())
with open("advisories.json") as f:
advisories = json.load(f)
with open("installed.txt") as f:
installed = [line.strip().rsplit("@", 1) for line in f if line.strip()]
print(f"\nShopNest: {len(installed)} installed versions, {len(advisories)} advisories")
findings = []
for adv in advisories:
for affected in adv["affected"]:
name, severity = affected["package"]["name"], adv["database_specific"]["severity"]
versions = [v for n, v in installed if n == name] # a package may be installed in two versions
if not versions:
print(f" {'not installed':<13} {name:<21} {adv['id']}")
for version in versions:
hits = [r["events"] for r in affected["ranges"] if in_range(version, r["events"])]
if hits:
fixed = next(e["fixed"] for e in hits[0] if "fixed" in e)
findings.append(name)
print(f" {'affected':<13} {name + '@' + version:<21} {adv['id']} {severity:<8} fixed in {fixed}")
else:
print(f" {'not affected':<13} {name + '@' + version:<21} {adv['id']} in none of its {len(affected['ranges'])} ranges")
print(f"{len(findings)} findings in {len(set(findings))} packages.") Output
Comparing versions as text instead of by SemVer gets some pairs wrong: is 6.9.7 below 6.14.1 SemVer: True text: False wrong is 8.10.0 below 8.4.0 SemVer: False text: True wrong is 2.0.0-rc.1 below 2.0.0 SemVer: True text: False wrong is 1.4.4-lts.1 below 1.4.5-lts.2 SemVer: True text: True ShopNest: 91 installed versions, 9 advisories affected lodash@4.17.20 GHSA-35jh-r3h4-6jhm HIGH fixed in 4.17.21 affected lodash@4.17.20 GHSA-f23m-r3pf-42rh MODERATE fixed in 4.18.0 affected multer@1.4.5-lts.2 GHSA-4pg4-qvpc-4q3h HIGH fixed in 2.0.0 affected body-parser@2.2.0 GHSA-wqch-xfxh-vrr4 MODERATE fixed in 2.2.1 affected qs@6.14.0 GHSA-6rw7-vpxm-498p MODERATE fixed in 6.14.1 affected path-to-regexp@8.2.0 GHSA-27v5-c462-wpq7 MODERATE fixed in 8.4.0 not affected path-to-regexp@8.2.0 GHSA-9wv6-86v2-598j in none of its 5 ranges not affected minimist@1.2.8 GHSA-xvch-5gv4-984h in none of its 2 ranges not installed semver GHSA-c2qf-rxjj-qqgw 6 findings in 5 packages.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 match.py
JSON · ranges/advisories.json
[
{
"id": "GHSA-35jh-r3h4-6jhm",
"summary": "Command Injection in lodash",
"database_specific": { "severity": "HIGH" },
"affected": [{
"package": { "ecosystem": "npm", "name": "lodash" },
"ranges": [
{ "type": "SEMVER", "events": [{ "introduced": "0" }, { "fixed": "4.17.21" }] }
]
}]
},
{
"id": "GHSA-f23m-r3pf-42rh",
"summary": "lodash vulnerable to Prototype Pollution via array path bypass in _.unset and _.omit",
"database_specific": { "severity": "MODERATE" },
"affected": [{
"package": { "ecosystem": "npm", "name": "lodash" },
"ranges": [
{ "type": "SEMVER", "events": [{ "introduced": "0" }, { "fixed": "4.18.0" }] }
]
}]
},
{
"id": "GHSA-4pg4-qvpc-4q3h",
"summary": "Multer vulnerable to Denial of Service from maliciously crafted requests",
"database_specific": { "severity": "HIGH" },
"affected": [{
"package": { "ecosystem": "npm", "name": "multer" },
"ranges": [
{ "type": "SEMVER", "events": [{ "introduced": "1.4.4-lts.1" }, { "fixed": "2.0.0" }] }
]
}]
},
{
"id": "GHSA-wqch-xfxh-vrr4",
"summary": "body-parser is vulnerable to denial of service when url encoding is used",
"database_specific": { "severity": "MODERATE" },
"affected": [{
"package": { "ecosystem": "npm", "name": "body-parser" },
"ranges": [
{ "type": "SEMVER", "events": [{ "introduced": "2.2.0" }, { "fixed": "2.2.1" }] }
]
}]
},
{
"id": "GHSA-6rw7-vpxm-498p",
"summary": "qs's arrayLimit bypass in its bracket notation allows DoS via memory exhaustion",
"database_specific": { "severity": "MODERATE" },
"affected": [{
"package": { "ecosystem": "npm", "name": "qs" },
"ranges": [
{ "type": "SEMVER", "events": [{ "introduced": "0" }, { "fixed": "6.14.1" }] }
]
}]
},
{
"id": "GHSA-27v5-c462-wpq7",
"summary": "path-to-regexp vulnerable to Regular Expression Denial of Service via multiple wildcards",
"database_specific": { "severity": "MODERATE" },
"affected": [{
"package": { "ecosystem": "npm", "name": "path-to-regexp" },
"ranges": [
{ "type": "SEMVER", "events": [{ "introduced": "8.0.0" }, { "fixed": "8.4.0" }] }
]
}]
},
{
"id": "GHSA-9wv6-86v2-598j",
"summary": "path-to-regexp outputs backtracking regular expressions",
"database_specific": { "severity": "HIGH" },
"affected": [{
"package": { "ecosystem": "npm", "name": "path-to-regexp" },
"ranges": [
{ "type": "SEMVER", "events": [{ "introduced": "0" }, { "fixed": "0.1.10" }] },
{ "type": "SEMVER", "events": [{ "introduced": "0.2.0" }, { "fixed": "1.9.0" }] },
{ "type": "SEMVER", "events": [{ "introduced": "2.0.0" }, { "fixed": "3.3.0" }] },
{ "type": "SEMVER", "events": [{ "introduced": "4.0.0" }, { "fixed": "6.3.0" }] },
{ "type": "SEMVER", "events": [{ "introduced": "7.0.0" }, { "fixed": "8.0.0" }] }
]
}]
},
{
"id": "GHSA-xvch-5gv4-984h",
"summary": "Prototype Pollution in minimist",
"database_specific": { "severity": "CRITICAL" },
"affected": [{
"package": { "ecosystem": "npm", "name": "minimist" },
"ranges": [
{ "type": "SEMVER", "events": [{ "introduced": "0" }, { "fixed": "0.2.4" }] },
{ "type": "SEMVER", "events": [{ "introduced": "1.0.0" }, { "fixed": "1.2.6" }] }
]
}]
},
{
"id": "GHSA-c2qf-rxjj-qqgw",
"summary": "semver vulnerable to Regular Expression Denial of Service",
"database_specific": { "severity": "HIGH" },
"affected": [{
"package": { "ecosystem": "npm", "name": "semver" },
"ranges": [
{ "type": "SEMVER", "events": [{ "introduced": "2.0.0-alpha" }, { "fixed": "5.7.2" }] },
{ "type": "SEMVER", "events": [{ "introduced": "6.0.0" }, { "fixed": "6.3.1" }] },
{ "type": "SEMVER", "events": [{ "introduced": "7.0.0" }, { "fixed": "7.5.2" }] }
]
}]
}
] Text · ranges/installed.txt
accepts@2.0.0
append-field@1.0.0
body-parser@2.2.0
buffer-from@1.1.2
busboy@1.6.0
bytes@3.1.2
call-bind-apply-helpers@1.0.2
call-bound@1.0.4
concat-stream@1.6.2
content-disposition@1.1.0
content-type@1.0.5
content-type@2.1.0
cookie-signature@1.2.2
cookie@0.7.2
core-util-is@1.0.3
debug@4.4.3
depd@2.0.0
dunder-proto@1.0.1
ee-first@1.1.1
encodeurl@2.0.0
es-define-property@1.0.1
es-errors@1.3.0
es-object-atoms@1.1.2
escape-html@1.0.3
etag@1.8.1
express@5.1.0
finalhandler@2.1.1
forwarded@0.2.0
fresh@2.0.0
function-bind@1.1.2
get-intrinsic@1.3.0
get-proto@1.0.1
gopd@1.2.0
has-symbols@1.1.0
hasown@2.0.4
helmet@8.1.0
http-errors@2.0.1
iconv-lite@0.6.3
iconv-lite@0.7.3
inherits@2.0.4
ipaddr.js@1.9.1
is-promise@4.0.0
isarray@1.0.0
lodash@4.17.20
math-intrinsics@1.1.0
media-typer@0.3.0
media-typer@1.1.1
merge-descriptors@2.0.0
mime-db@1.52.0
mime-db@1.54.0
mime-types@2.1.35
mime-types@3.0.2
minimist@1.2.8
mkdirp@0.5.6
ms@2.1.3
multer@1.4.5-lts.2
negotiator@1.1.0
object-assign@4.1.1
object-inspect@1.13.4
on-finished@2.4.1
once@1.4.0
parseurl@1.3.3
path-to-regexp@8.2.0
process-nextick-args@2.0.1
proxy-addr@2.0.8
qs@6.14.0
range-parser@1.3.0
raw-body@3.0.2
readable-stream@2.3.8
router@2.2.0
safe-buffer@5.1.2
safer-buffer@2.1.2
send@1.2.1
serve-static@2.2.1
setprototypeof@1.2.0
side-channel-list@1.0.1
side-channel-map@1.0.1
side-channel-weakmap@1.0.2
side-channel@1.1.1
statuses@2.0.2
streamsearch@1.1.0
string_decoder@1.1.1
toidentifier@1.0.1
type-is@1.6.18
type-is@2.1.0
typedarray@0.0.6
unpipe@1.0.0
util-deprecate@1.0.2
vary@1.1.2
wrappy@1.0.2
xtend@4.0.2 Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Six findings in five packages, each with the version that fixes it. path-to-regexp 8.2.0 shows how ranges work: it is outside all five ranges of GHSA-9wv6-86v2-598j, because 8.0.0 fixed that flaw for the line it is on, yet inside GHSA-27v5-c462-wpq7, a later flaw that runs from 8.0.0 up to 8.4.0. minimist 1.2.8 is above the fixed 1.2.6, and semver is not installed. GHSA-f23m-r3pf-42rh adds a warning of its own: it records that the fix for an earlier lodash flaw could be bypassed, that versions up to 4.17.23 are affected, and that 4.18.0 fixes it. A scan is true for the day it runs, which is why OWASP asks for advisories to be monitored continuously.
Comparing versions as text
As text, 6.9.7 sorts after 6.14.1 and 8.10.0 before 8.4.0, so a text comparison misses an affected version and flags a fixed one: three of the run’s four pairs came out wrong. Compare the numbers part by part, and put a pre-release such as 2.0.0-rc.1 before its release, as SemVer 2.0.0 does.
Scanners do this matching for you against their own databases. npm audit sends the names and versions in the
lockfile to the registry, osv-scanner scan -L package-lock.json checks a lockfile against OSV, and
pip-audit -r requirements.txt checks a Python requirements file. Knowing the rules lets you check a surprising result
instead of taking it on trust.
Checkpoint
Check: Reading an advisory's version ranges
Triage by exploitation, exposure and reach, then set a deadline
Six findings are not six emergencies. Four questions put them in order:
- Is it being exploited? CISA’s KEV catalogue: CISA's Known Exploited Vulnerabilities catalogue, the list of vulnerabilities known to have been exploited in the wild. CISA advises using it as an input when deciding which fixes come first. catalogue lists flaws exploited in the wild, and CISA advises using it as an input to prioritising. FIRST’s EPSS: The Exploit Prediction Scoring System, FIRST's model that estimates the probability, from 0 to 1, that a published CVE will be exploited in the wild in the next 30 days. estimates the probability, from 0 to 1, that a CVE will be exploited in the next 30 days.
- Is the component exposed? ShopNest’s upload and form parsing, multer, body-parser and qs, read requests from the internet.
- Does your code reach the flaw? GHSA-35jh-r3h4-6jhm is in lodash’s template function. If ShopNest never calls it, the version still needs fixing, but the finding does not outrank one the internet can reach.
- Is there a fix? All six findings have one. Where none exists, the plan is a mitigation, not a date.
The deadlines themselves belong in a documented policy, which ASVS asks for in 15.1.1 and checks in 15.2.1. CERT-In’s Blueprint for Reducing Exposure and Defending against AI-Assisted Vulnerabilities Exploitation in Digital Infrastructure gives indicative remediation timelines that make a sound starting point:
| Finding | Indicative expectation |
|---|---|
| Known exploited, on internet-facing crown-jewel systems | Contain at once; patch, mitigate or remove the exposure within 12 hours where feasible |
| Critical and externally exposed | Patch, mitigate or remove the exposure within 1 day |
| Known exploited, on internal systems | Within 1 day, unless compensating controls are in place and documented |
| Critical and internal, on high-value systems | Within 3 days |
| High severity | Within 5 days, by risk |
| No patch available | Isolation, access restriction, WAF or API protection, closer monitoring, or the feature switched off |
They are an Indian authority’s indicative targets, not a law for every application. Record the reasoning as well as
the result. A VEX: Vulnerability Exploitability eXchange: a statement of whether a product is affected by a vulnerability, with a status such as not affected, affected, fixed or under investigation, so that users can set aside findings that do not apply. statement says whether a product is not affected, affected, fixed or still
under investigation; OSV-Scanner reads exceptions with a reason and an expiry date from osv-scanner.toml; and
npm audit --audit-level=high fails a build only on high and critical findings while still reporting every one. That
is how a team avoids alert fatigue without hiding anything.
Key idea
Rank findings by exploitation, exposure, reach and whether a fix exists, give each a deadline from a documented policy, and write down why any finding was set aside.
Checkpoint
Check: Ranking findings and setting deadlines
Upgrade, override, replace or remove: choosing the fix
ShopNest’s six findings need three different fixes. lodash is pinned exactly at 4.17.20 in package.json, so its fix is
an edit of that line to 4.18.0, the version that fixes both of its records, and a test run. body-parser 2.2.0, qs
6.14.0 and path-to-regexp 8.2.0 are transitive, and each fix lies inside a range express 5.1.0 already declares:
^2.2.0, ^6.14.0, and ^8.0.0 through router. npm update body-parser qs path-to-regexp therefore moves them up
in the lockfile without touching package.json. multer is fixed only in 2.0.0, a new major version, so it needs code
changes and tests, which its own deprecation message already asks for.
When a fixed version lies outside the range a parent declares, the overrides field of package.json replaces the
version throughout the tree; npm’s documentation names exactly this use, replacing a dependency with a known security
issue. An override pins a version the parent was never tested with, so test the paths that use it and write down why
it is there. When no fix exists, OWASP’s cheat sheet on vulnerable dependencies lists what remains: a workaround or
temporary safeguards until a patch arrives, and a patch of your own as the last resort. When a fix exists but the
version you run cannot take it, the cheat sheet’s answer is a backport of that fix.
The cheapest dependency to fix is the one you removed, and OWASP’s list of defences includes trimming unused dependencies, features, components and files. Answer four questions about one finding, and the table shows which fixes fit:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Fix | Work today | Chance of breaking the app | How long it holds |
|---|---|---|---|
| Refresh the lockfile How: npm update with the package name | Good one command: fixes body-parser, qs and path-to-regexp inside express 5.1.0's ranges | Good versions the parent already accepts, so tests usually pass | Good until the next advisory; package.json is unchanged |
| Raise a direct version How: its entry in package.json | Good edit one line: lodash 4.17.20 to 4.18.0 | Mixed a minor or patch release, still worth a test run | Good as long as the package keeps shipping fixes |
| Override a version How: overrides in package.json | Mixed one entry, but it pins a version the parent never asked for | Mixed the parent may not work with it: test the paths that use it | Poor until someone forgets why it is there: write the reason down |
| Upgrade a major version How: new API: code changes and tests | Poor multer 1 to 2: read the changes and adapt the upload code | Poor API changes can break routes that worked | Good the supported line, where fixes keep arriving |
| Replace the package How: another library or the runtime | Poor new code and new tests for what the package did | Mixed a rewrite of one feature, tested like any change | Good removes a package that keeps causing findings |
| Remove it How: delete what nothing uses | Good delete it and its imports, if nothing uses it | Good none, if the tests prove nothing used it | Good for good: no package, no findings |
| Mitigate for now How: isolate, restrict, filter, monitor | Mixed a firewall rule, a feature switched off or a filter in front | Mixed can block real traffic if the rule is too wide | Poor a stopgap until a fixed version exists |
How the answers decide
- “Does your code still use the package, directly or through something it needs?” No, nothing uses it: rules out Refresh the lockfile, Raise a direct version, Override a version, Upgrade a major version, Replace the package and Mitigate for now. Nothing uses it, so delete it: OWASP asks for unused dependencies to be removed, and a package that is gone has no findings.
- “Does a fixed version exist for this finding?” No fix yet: rules out Refresh the lockfile, Raise a direct version, Override a version and Upgrade a major version and adds a caveat to Mitigate for now. No version fixes it yet: reduce the exposure now, replace or remove the package if you can, and watch the advisory.
- “Does a fixed version exist for this finding?” Yes and “Does the fixed version fit the range already declared for it?” Yes, inside the range: rules out Override a version and Upgrade a major version. The fix is inside the declared range: refreshing the lockfile, or raising your own entry, fixes it with the least risk.
- “Does the fixed version fit the range already declared for it?” No, outside it: rules out Refresh the lockfile and adds a caveat to Override a version. The declared range excludes the fix: raise the package that declares it, or override and test that the parent still works.
- “Is the fix only in a new major version?” Yes, a new major: rules out Refresh the lockfile and Raise a direct version and adds a caveat to Upgrade a major version. Only a new major fixes it: plan the code changes and tests, as multer 1 to 2 needs, or replace the package.
In short: refresh the lockfile when the fix is inside the declared range, raise your own entry when the package is yours to change, upgrade or replace when only a new major fixes it, and mitigate only while no fix exists. Roll every fix out in stages, as OWASP advises, so that one bad release never reaches every system at once.
Checkpoint
Check: Choosing the fix for a finding
Practise: turn the findings into a dated plan
Write the triage this lesson did by hand: match the advisories by their ranges, find each fix, and date every finding by ShopNest’s policy. The sample tests cover the edges of a range, versions that sort wrongly as text, a package installed twice and every row of the policy:
Exercise · Medium · Python
Turn an SBOM and advisories into a dated plan
Write triage(sbom, advisories, context) in triage.py. It returns ShopNest's work list: one finding for every installed version that an advisory affects, each with the version that fixes it and a deadline in hours.
sbomis a CycloneDX dictionary:sbom["components"]is a list of{"name": …, "version": …}. A package may appear twice, in two versions.advisoriesis a list of OSV records, as in the lesson: anid,database_specific["severity"](CRITICAL,HIGH,MODERATEorLOW) andaffected, each with apackagename and itsrangesofevents.contexthasinternet_facing(a bool),kev(the ids of advisories whose vulnerability is in CISA's Known Exploited Vulnerabilities catalogue) andnot_affected(a dictionary of advisory ids the team has recorded as not affecting ShopNest, each with its reason).
Match versions as OSV does: introduced and last_affected include the version they name, fixed does not, "0" is before every version, and a version is affected if it falls in any one of the ranges. Compare versions with key(), which the starter gives you. The fix is the fixed version of the range the installed version fell in, or None when that range has no fixed event.
ShopNest's documented deadlines, set after CERT-In's indicative timelines: 12 hours for a KEV advisory on an internet-facing app; 24 hours for a CRITICAL one on an internet-facing app, or a KEV one on an internal app; 72 hours for a CRITICAL one on an internal app; 120 hours for HIGH; 720 hours for the rest. Leave out advisories recorded as not affected.
Return a list of dictionaries with the keys id, package, version, fixed and hours, sorted by hours, then by id, then by version.
Starter code · triage.py
def key(version):
"""A sort key in SemVer 2.0.0 order (given): parts compare as numbers, and 2.0.0-rc.1 comes before 2.0.0."""
core, _, pre = version.split("+")[0].partition("-")
numbers = tuple(int(part) for part in core.split("."))
if not pre:
return numbers + ((1,),)
labels = tuple((0, int(p)) if p.isdigit() else (1, p) for p in pre.split("."))
return numbers + ((0, *labels),)
def triage(sbom, advisories, context):
"""Starter: it matches advisories by package name only and dates everything by severity alone."""
findings = []
for component in sbom["components"]:
for adv in advisories:
for affected in adv["affected"]:
if affected["package"]["name"] == component["name"]:
hours = 120 if adv["database_specific"]["severity"] == "HIGH" else 720
findings.append({"id": adv["id"], "package": component["name"], "version": component["version"], "fixed": None, "hours": hours})
return findings The sample tests · test_triage.py
from triage import triage
# Made-up packages and advisory ids: the tests check the rules, not any real package.
EVERY_VERSION = [{"introduced": "0"}]
PUBLIC = {"internet_facing": True, "kev": [], "not_affected": {}}
INTERNAL = {"internet_facing": False, "kev": [], "not_affected": {}}
def adv(id, name, severity, *ranges):
ranges = [{"type": "SEMVER", "events": list(events)} for events in ranges]
return {"id": id, "database_specific": {"severity": severity}, "affected": [{"package": {"ecosystem": "npm", "name": name}, "ranges": ranges}]}
def sbom(*pairs):
return {"components": [{"name": n, "version": v} for n, v in pairs]}
def test_introduced_counts_its_own_version_and_fixed_does_not():
body = adv("T-1", "form-reader", "MODERATE", [{"introduced": "2.2.0"}, {"fixed": "2.2.1"}])
assert triage(sbom(("form-reader", "2.2.0")), [body], PUBLIC) == [{"id": "T-1", "package": "form-reader", "version": "2.2.0", "fixed": "2.2.1", "hours": 720}]
assert triage(sbom(("form-reader", "2.2.1")), [body], PUBLIC) == [], "the fixed version is not affected"
assert triage(sbom(("form-reader", "2.1.9")), [body], PUBLIC) == [], "a version before introduced is not affected"
def test_versions_compare_as_semver_not_as_text():
a = adv("T-2", "query-split", "MODERATE", [{"introduced": "0"}, {"fixed": "6.14.1"}])
b = adv("T-3", "route-match", "MODERATE", [{"introduced": "8.0.0"}, {"fixed": "8.4.0"}])
c = adv("T-4", "upload-parts", "HIGH", [{"introduced": "1.4.4-lts.1"}, {"fixed": "2.0.0"}])
found = triage(sbom(("query-split", "6.9.7"), ("query-split", "6.14.1"), ("route-match", "8.10.0"), ("upload-parts", "2.0.0-rc.1"), ("upload-parts", "1.4.5-lts.2")), [a, b, c], PUBLIC)
assert sorted((f["package"], f["version"]) for f in found) == [("query-split", "6.9.7"), ("upload-parts", "1.4.5-lts.2"), ("upload-parts", "2.0.0-rc.1")]
def test_the_fix_comes_from_the_range_the_version_fell_in():
ranged = adv("T-5", "version-tools", "HIGH", [{"introduced": "2.0.0-alpha"}, {"fixed": "5.7.2"}], [{"introduced": "6.0.0"}, {"fixed": "6.3.1"}], [{"introduced": "7.0.0"}, {"fixed": "7.5.2"}])
found = triage(sbom(("version-tools", "6.2.0"), ("version-tools", "7.5.4")), [ranged], INTERNAL)
assert found == [{"id": "T-5", "package": "version-tools", "version": "6.2.0", "fixed": "6.3.1", "hours": 120}]
capped = adv("T-6", "old-helper", "LOW", [{"introduced": "1.0.0"}, {"last_affected": "1.4.2"}])
found = triage(sbom(("old-helper", "1.4.2"), ("old-helper", "1.4.3")), [capped], INTERNAL)
assert found == [{"id": "T-6", "package": "old-helper", "version": "1.4.2", "fixed": None, "hours": 720}]
def test_every_installed_version_is_checked():
kind = adv("T-7", "content-kind", "MODERATE", [{"introduced": "2.0.0"}, {"fixed": "2.1.1"}])
found = triage(sbom(("content-kind", "1.0.5"), ("content-kind", "2.1.0")), [kind], PUBLIC)
assert [(f["version"], f["fixed"]) for f in found] == [("2.1.0", "2.1.1")]
def test_deadlines_follow_the_documented_policy():
advisories = [adv(f"T-{n}", name, sev, EVERY_VERSION) for n, name, sev in [(10, "a", "MODERATE"), (11, "b", "HIGH"), (12, "c", "CRITICAL"), (13, "d", "LOW"), (14, "e", "HIGH")]]
components = sbom(*[(name, "1.0.0") for name in "abcde"])
context = {"internet_facing": True, "kev": ["T-10"], "not_affected": {"T-14": "the vulnerable function is never called"}}
found = triage(components, advisories, context)
assert [(f["id"], f["hours"]) for f in found] == [("T-10", 12), ("T-12", 24), ("T-11", 120), ("T-13", 720)]
internal = triage(components, advisories, {**context, "internet_facing": False})
assert [(f["id"], f["hours"]) for f in internal] == [("T-10", 24), ("T-12", 72), ("T-11", 120), ("T-13", 720)] A hint
Write in_range(version, events) first: sort the events with key() (put "0" first), start with hit = False, and for each event the version has reached, set hit to True for introduced and to False for fixed. Then loop over every component, not over a dictionary of names, so that a package installed twice is checked twice.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then work through the questions on inventories, SBOMs, ranges and deadlines, each one explained the moment you answer:
Practice that adapts to you
Practise this lesson
13 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: what is an SBOM, and what do you do with one?
An SBOM, a software bill of materials, is a machine-readable list of every component in a piece of software: each name and exact version, an identifier such as a package URL, hashes, licences and how the components depend on each other, in a standard format such as CycloneDX or SPDX. You generate it from the lockfile or the built artifact on every build, keep it with the release, and let monitoring check it against advisory databases every day, so that a new advisory becomes a list of affected releases instead of a search. Customers and regulators may ask for it too. Know its limits: an SBOM made from a lockfile cannot see what a container image adds, and generators often leave fields such as the supplier empty.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Your CI runs npm audit and stops a release that fixes a payment bug: it reports 37 findings, 4 of them high and none critical. Customers are waiting for the fix. How do you decide what must block this release and what can follow it?
-
Mid to staff
A critical advisory appears for a package three levels down your dependency tree. It lists no fixed version, and the package that pulls it in has not reacted. Your app is internet-facing. What do you do this week?
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does A03:2025 Software Supply Chain Failures cover, and why does testing data understate it?
Failures or compromises anywhere in the chain that builds, ships or updates your software, whether the cause is a flawed library, a hostile one or a weak build system; it widened the 2021 category on vulnerable and outdated components. Only 11 CVE records map to its six CWEs, yet half of the community survey ranked it first, because most supply chain failures never become a CVE in your own code.
-
Why is the lockfile your dependency inventory, and how do you build from it?
package.json holds names and ranges; the lockfile holds every installed package with its exact version, source and integrity hash. In the lesson, 4 requested packages became 91 versions, 87 of them transitive. Commit the lockfile, build with npm ci (it stops when the two files disagree) and scan the lockfile, or an SBOM made from it, so the tree you scan is the tree you ship.
-
What is an SBOM, and which formats and identifiers does it use?
A software bill of materials: the inventory in a standard format that scanners and customers can read, with each component named by a package URL such as pkg:npm/express@5.1.0. CycloneDX (an OWASP project, ECMA-424) and SPDX (ISO/IEC 5962:2021) lead. npm sbom writes CycloneDX from the lockfile but left every supplier empty, so check a generated SBOM against the fields you must supply; CERT-In's guidelines list 21 minimum fields.
-
What can an SBOM made from the repository not see?
What a container image adds later. A source SBOM is one of six kinds that CERT-In's guidelines describe, one for each stage of the life cycle. Keep a complete SBOM for internal use and share a customised one with consumers.
-
How do you decide whether a version is affected by an advisory written in OSV format?
Each range is a list of events in version order: introduced is the first affected version and counts, fixed is the first version without the flaw and does not count, last_affected is the last known affected version and counts, and "0" stands for before all versions. A version is affected when it falls in any one range, and a flaw often has one range per release line. Compare by SemVer rules, not as text: as text, 8.10.0 sorts before 8.4.0.
-
What four questions rank dependency findings, and what then?
Is it being exploited (CISA's KEV catalogue; FIRST's EPSS, the probability of exploitation within 30 days)? Is the component exposed to the internet? Does your code reach the flaw? Is there a fix? Give each finding a deadline from a documented policy (ASVS 5.0.0 15.1.1, checked by 15.2.1) and record why any finding is set aside, for example in a VEX statement.
-
Which fix suits a vulnerable dependency, depending on where the fix lies?
Inside the range a parent declares: refresh the lockfile (npm update). A version you pinned yourself: edit your own entry. Outside the parent's range: overrides in package.json, tested and justified. Only in a new major version: upgrade or replace, with code changes. No fix yet: a mitigation. The cheapest dependency to fix is the one you removed.
Key takeaways
- OWASP’s A03:2025 covers every part software is built from, its packages, tools, pipeline and updates, and testing data understates it: only 11 CVE records map to its CWEs.
- The lockfile is the inventory: ShopNest’s 4 requests became 91 versions, 87 of them transitive, so scans read the lockfile or an SBOM made from it.
- An SBOM writes that inventory in CycloneDX or SPDX, with package URLs and hashes; npm’s generator left the supplier empty for all 91 components, so check a generated SBOM against the fields you need.
- An advisory affects a version only inside one of its ranges, compared by SemVer: introduced counts its own version, and fixed does not.
- Findings are ranked by exploitation, exposure, reach and whether a fix exists, and dated by a documented policy such as CERT-In’s indicative timelines.
- The fix follows the finding: refresh the lockfile inside the range, raise a version you declared, upgrade or replace across a major, and mitigate only while no fix exists.
Terms in this lesson
- EPSS
- The Exploit Prediction Scoring System, FIRST's model that estimates the probability, from 0 to 1, that a published CVE will be exploited in the wild in the next 30 days.
- KEV catalogue
- CISA's Known Exploited Vulnerabilities catalogue, the list of vulnerabilities known to have been exploited in the wild. CISA advises using it as an input when deciding which fixes come first.
- lockfile
- A file the package manager writes, such as package-lock.json, that records the exact version, source and integrity hash of every installed package, so that every install of the project gets the same tree.
- package URL (purl)
- A standard identifier for a package in any ecosystem, such as pkg:npm/express@5.1.0: the type, an optional namespace, the name and the version, with optional qualifiers. Ecma standardised it as ECMA-427.
- SBOM (software bill of materials)
- A machine-readable inventory of the components in a piece of software: each name, exact version, identifier and hash, and how they depend on each other, written in a standard format such as CycloneDX or SPDX.
- transitive dependency
- A package your project does not name itself but that one of its dependencies, or theirs, needs. A lockfile records each one with its exact version; a scan that reads only the project's manifest misses them.
- VEX
- Vulnerability Exploitability eXchange: a statement of whether a product is affected by a vulnerability, with a status such as not affected, affected, fixed or under investigation, so that users can set aside findings that do not apply.
References
- A03:2025 Software Supply Chain Failures (OWASP Foundation)
- Application Security Verification Standard 5.0.0: V15 Secure Coding and Architecture (OWASP Foundation)
- CWE-1395: Dependency on Vulnerable Third-Party Component (MITRE)
- CWE-1104: Use of Unmaintained Third Party Components (MITRE)
- CycloneDX specification overview (OWASP CycloneDX)
- ECMA-424: CycloneDX Bill of materials specification (Ecma International)
- The System Package Data Exchange (SPDX) Specification 3.0.1 (SPDX project (The Linux Foundation))
- SPDX specifications (SPDX project (The Linux Foundation))
- ECMA-427: Package-URL (PURL) specification (Ecma International)
- npm-sbom (npm Docs)
- package-lock.json (npm Docs)
- npm-ci (npm Docs)
- npm-audit (npm Docs)
- npm-update (npm Docs)
- package.json: license and overrides (npm Docs)
- Open Source Vulnerability format (OSV schema) (Open Source Security Foundation (OpenSSF))
- OSV.dev, a distributed vulnerability database for open source (Open Source Security Foundation (OpenSSF))
- GitHub Advisory Database (GitHub)
- OSV-Scanner usage (OSV-Scanner project)
- OSV-Scanner configuration (OSV-Scanner project)
- pip-audit (Python Package Index (pip-audit maintainers))
- Technical Guidelines on SBOM, QBOM and CBOM, AIBOM and HBOM (version 2.0) (Indian Computer Emergency Response Team (CERT-In))
- Blueprint for Reducing Exposure and Defending against AI-Assisted Vulnerabilities Exploitation in Digital Infrastructure (version 1.0) (Indian Computer Emergency Response Team (CERT-In))
- Known Exploited Vulnerabilities Catalog (Cybersecurity and Infrastructure Security Agency (CISA))
- Exploit Prediction Scoring System (EPSS) (FIRST)
- Vulnerable Dependency Management Cheat Sheet (OWASP Foundation)
- Semantic Versioning 2.0.0 (semver.org)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress