The npm node-fetch package has two noteworthy CVEs in its history — a high-severity credential leak on cross-origin redirects (CVE-2022-0235) and a size-limit bypass (CVE-2020-15168) — and with fetch now built into Node.js, most projects can retire the dependency entirely. node-fetch was the de facto standard HTTP client for a decade and remains one of the most-downloaded packages on the registry, mostly through transitive edges in older trees. This post covers what actually went wrong in it, which versions are safe if you must keep it, and a practical migration path to the platform-native API.
Why node-fetch became ubiquitous
Before Node.js had a built-in fetch, every server-side project that wanted the browser's familiar fetch() semantics reached for node-fetch. It was small, promise-based, and API-compatible enough that isomorphic code could run on both sides. That success is why it still shows up in dependency trees everywhere: SDKs, CLI tools, and frameworks adopted it years ago, and transitive dependencies outlive fashion. Run npm ls node-fetch in any mature repository and you will usually find it two or three levels down, often in multiple versions at once.
That persistence is exactly why its vulnerability history still matters in 2025, long after new code stopped importing it.
CVE-2022-0235: secure headers forwarded to untrusted hosts
The serious one. node-fetch before 2.6.7, and 3.x before 3.1.1, forwarded security-sensitive headers — authorization, www-authenticate, cookie, and cookie2 — when following a redirect to a different, untrusted origin. GitHub's advisory (GHSA-r683-j2x4-v87g) rates it high severity with a CVSS score of 8.8.
The attack shape is simple. Your service calls an API with a bearer token:
const res = await fetch("https://api.partner.example/v1/data", {
headers: { authorization: `Bearer ${serviceToken}` },
});
If that endpoint returns a 302 pointing at https://evil.example/collect — because the partner is compromised, or because the URL was attacker-influenced in the first place (an SSRF-adjacent pattern) — vulnerable node-fetch versions replayed your authorization header to the new host. The attacker's server logs your credential. Browsers strip credentials on cross-origin redirects for exactly this reason; node-fetch's redirect handling did not, until the fix.
If you cannot upgrade immediately, the mitigations are redirect: "manual" (handle redirects yourself and re-attach credentials only to hosts you trust) or an allowlist check on the redirect target. But upgrading is a patch-level bump on the 2.x line; there is rarely a reason not to.
CVE-2020-15168: size limits silently ignored after redirect
The lesser-known one. node-fetch supports a size option to cap response bodies — a defense against memory exhaustion when fetching untrusted URLs. Before 2.6.1 (and 3.0.0-beta.9), that limit was not honored after following a redirect: a response over the limit never threw the expected FetchError, so an attacker who controlled the target could feed you an arbitrarily large body through one redirect hop. The impact is denial of service by memory pressure rather than data theft, which is why it carried a lower severity, but it is a good illustration of a recurring theme: redirect handling is where HTTP client bugs live. Both node-fetch CVEs are redirect bugs, and the same is true across other ecosystems' clients.
The safe versions, and the maintenance reality
If node-fetch stays in your tree, the floor is 2.6.7 on the 2.x line or 3.1.1 on 3.x; anything current on either line includes both fixes. The 3.x line is ESM-only, which is the main reason so much CommonJS code pinned 2.x forever — and why old, sometimes vulnerable 2.x versions keep circulating in transitive dependencies. Development activity on the project has slowed substantially now that the platform provides fetch natively, which is not an emergency but is a data point: bug fixes and future advisories will move slower than they did at the project's peak.
This is the classic case where a software composition analysis scan earns its keep: you almost certainly do not depend on node-fetch directly, but some SDK three levels down does, and only a full-tree scan will tell you whether it resolved to a pre-2.6.7 version. An SCA tool such as Safeguard flags the vulnerable range and, just as usefully, shows you which direct dependency dragged it in so you know what to actually upgrade.
Native fetch in Node.js: the upgrade path
Node.js has shipped a built-in fetch (powered by the undici HTTP client) enabled by default since Node 18, with the API marked stable in Node 21. Every currently supported Node release has it. For most code, migration is deletion:
// before
const fetch = require("node-fetch"); // or: import fetch from "node-fetch"
const res = await fetch(url);
// after — no import at all
const res = await fetch(url);
Things to check while migrating, since "fetch node" compatibility is close but not identical:
- Streams.
res.bodyis a WHATWGReadableStream, not a Node stream. Where you piped before, useReadable.fromWeb(res.body)fromnode:streamor iterate withfor await. - The size option. Native fetch has no
size; enforce body limits by counting bytes as you consume the stream and aborting viaAbortController. - Timeouts. Use
AbortSignal.timeout(ms)as thesignal— node-fetch'stimeoutoption does not exist natively. - Agents and proxies. node-fetch accepted a custom
agent; native fetch uses undici dispatchers (ProxyAgent, connection pool options) instead. This is the one area where migration takes real work. - Redirect credential behavior. Native fetch strips
authorizationon cross-origin redirects — the safe behavior CVE-2022-0235 was about — so if you relied on header forwarding across hosts, you will need explicit re-authentication logic.
Ship the migration behind your normal test suite plus one integration test that exercises a redirect, a timeout, and a streamed body; those three cover nearly all behavioral drift.
What this history teaches about dependency strategy
node-fetch is a success story that aged into a liability pattern: a polyfill-class dependency that the platform absorbed. The security posture practically improves twice when you remove it — you shed the package's own CVE surface, and you move HTTP behavior onto code maintained by the Node.js project with platform-level scrutiny. The same review is worth running across your tree for other absorbed polyfills; the dependency-audit modules in Safeguard Academy include a checklist for exactly this "platform has caught up" sweep. Fewer dependencies is the cheapest vulnerability management there is.
FAQ
Is node-fetch still safe to use?
Yes, on patched versions: at least 2.6.7 on the CommonJS 2.x line or 3.1.1 on the ESM 3.x line. Both known CVEs (CVE-2022-0235 header forwarding, CVE-2020-15168 size-limit bypass) are fixed there. The project is in maintenance-mode territory, so plan an eventual move to native fetch.
What was the node-fetch vulnerability CVE-2022-0235?
Versions before 2.6.7 and 3.1.1 forwarded secure headers (authorization, cookie, www-authenticate, cookie2) when following a redirect to a different origin, letting an attacker who controlled a redirect target capture credentials. It was rated high severity (CVSS 8.8).
Do I still need node-fetch in modern Node.js?
Usually not. Node 18 and later ship a native fetch built on undici, stable since Node 21. New code should use the built-in; existing code can migrate with minor changes around streams, timeouts, and proxy agents.
How do I find out if node-fetch is in my dependency tree?
Run npm ls node-fetch (or the yarn/pnpm equivalent) to see every resolved version and which direct dependencies pull it in. An SCA scanner will additionally match those versions against the CVE ranges and alert on pre-2.6.7/3.1.1 resolutions.