Node.js began in 2009 with 0.x releases, split when the io.js fork shipped in January 2015, and reunited as Node.js 4.0.0 in September 2015. Since then even numbered major versions have become Long Term Support lines with 30 months of support, security fixes ship in batched, pre announced releases, and an opt in permission model limits what trusted code can touch.
That is the short answer. The rest of this post is the long one: a dated timeline of every major line from 0.10 to the new Node.js 27 Alpha channel, the OpenSSL version each line carries, what the permission model actually restricts (and what the project itself says it does not protect against), how the security team runs a release, and a step by step plan for finding every Node.js runtime you ship and moving it to a supported line. Every date below comes from the Node.js Release working group's schedule.json, the official distribution index at nodejs.org, the Node.js changelogs or the Node.js API docs. For the current support dates of each line in one place, see our companion post on how long a Node.js LTS release is supported.
Key takeaways
- Two projects became one. io.js shipped its first release, 1.0.0, on 14 January 2015 and its last, 3.3.1, on 15 September 2015. Node.js 4.0.0, released 8 September 2015, combined both codebases under the Node.js Foundation, which is why there is no Node.js 1, 2 or 3.
- Even lines get LTS, odd lines do not (until 27). Through Node.js 26, even numbered lines ship in April, enter Active LTS in October, and reach end of life about 30 months after LTS starts. Odd numbered lines live roughly eight months and never get LTS.
- The schedule changes in October 2026. From Node.js 27 there is one major release a year, every line becomes LTS, and a six month Alpha phase replaces the odd numbered releases. Node.js 27 Alpha is scheduled to start on 28 October 2026.
- Supported lines today (25 October 2026): 22.x (Maintenance LTS until 30 April 2027), 24.x (moved from Active LTS to Maintenance on 20 October 2026, end of life 30 April 2028) and 26.x (Current, scheduled for Active LTS on 28 October 2026, end of life 30 April 2029). Node.js 20 reached end of life on 30 April 2026.
- The permission model is a seat belt, not a sandbox. The official docs say it "does not protect against malicious code." It stops trusted code from accidentally reaching files, the network, child processes, workers and other resources you did not grant.
- Security releases are batched and pre announced. Fixes are prepared privately for every supported line, held until an embargo date, then published together with an advisory on the Node.js blog.
- Node.js ships its own copies of OpenSSL, llhttp, undici, npm and more. A Node.js patch release can fix a vulnerability in one of those bundled components, and your scanners need to see the Node.js version to know it.
How did Node.js start, and what was io.js?
2009 to 2014: the 0.x years
Node.js runs JavaScript outside the browser on Google's V8 engine, with an event loop and non blocking I/O at its core. The project's original 0.x ChangeLog, preserved in the nodejs/node-v0.x-archive repository, records its earliest tagged versions in June 2009: 0.0.3 on 11 June 2009 and 0.1.0 on 30 June 2009. Version 0.2.0 followed on 20 August 2010. For its first six years every release carried a 0.x version number.
The Release working group's own schedule file starts its history at the 0.8 line (25 June 2012 to 31 July 2014), followed by 0.10 (11 March 2013 to 31 October 2016) and 0.12 (6 February 2015 to 31 December 2016). None of these lines had a codename or an Active LTS phase. The working group's end of life table does give 0.10 and 0.12 a Maintenance start, 1 October 2015 and 1 April 2016, which carried both lines to an orderly end of life after the merge.
The official distribution index flags six 0.x releases as security releases. The last 0.12 build, 0.12.18 on 22 February 2017, bundles OpenSSL 1.0.1u, and nothing in it will change again.
The io.js fork
io.js was a fork of Node.js that ran as a separate project for most of 2015. According to the io.js distribution index, which is still online at iojs.org, io.js 1.0.0 shipped on 14 January 2015, and the project published 41 releases in total, ending with io.js 3.3.1 on 15 September 2015. Its version numbers moved quickly through 1.x, 2.x and 3.x in a single year, tracking newer V8 releases than Node.js 0.12 carried at the time.
The fork matters to anyone reading old build files today. A project that pins iojs or a 1.x to 3.x runtime is pinned to io.js, not Node.js, and that runtime has not received a fix since September 2015. The iojs.org home page now simply serves the Node.js site.
The merge and Node.js 4.0.0
The two projects reconciled under the newly formed Node.js Foundation. On 14 September 2015 the Foundation announced Node.js 4.0.0, which "combines both the Node.js project and io.js project in a single codebase." The announcement explains the version number directly: it was "named version 4.0.0 because it includes major updates from io.js version 3.0.0." The same release brought V8 4.5, a new test cluster, ARM support and, for the first time, a promise of long term support. The Release working group's schedule dates 4.x to 8 September 2015, with LTS (codename Argon) starting in October 2015.
From the Node.js Foundation to the OpenJS Foundation
On 12 March 2019 the Node.js Foundation and the JS Foundation announced that they were merging to form the OpenJS Foundation, according to the Linux Foundation's press release from the Open Source Leadership Summit that day. The OpenJS Foundation hosts Node.js today. The footer of every nodejs.org page now reads "Copyright OpenJS Foundation and Node.js contributors," and the project's security policy lists the OpenJS Foundation CNA (CVE Numbering Authority) at security@lists.openjsf.org as the escalation path when a report goes unanswered.
How does the Node.js release schedule work?
The model that applies to Node.js 4 through 26
The Release working group's README describes the model plainly. New semver major releases were branched from main every six months: even numbered versions in April, odd numbered versions in October. When a new odd numbered line came out, the previous even numbered line transitioned to Long Term Support.
Each release line then moved through these phases:
| Phase | What lands | Who should run it |
|---|---|---|
| Current | Most non breaking changes from main | Library authors, early adopters, CI testing |
| Active LTS | Features, fixes and updates audited by the Release team as stable for the line | Production |
| Maintenance LTS | Critical bug fixes and security updates; features only when they help migration | Production that has not yet moved |
| End of life | Nothing, including security fixes | Nobody |
From Node.js 12 onward an LTS line stays in Active LTS for 12 months and then in Maintenance for 18 months, so 30 months of LTS in total. Before Node.js 12 the split was the other way round: 18 months active and 12 months maintenance. Odd numbered lines never became LTS. They were Current for about six months and reached end of life roughly two months after entering a short maintenance window.
Each LTS line also gets a codename, in alphabetical order: Argon (4), Boron (6), Carbon (8), Dubnium (10), Erbium (12), Fermium (14), Gallium (16), Hydrogen (18), Iron (20), Jod (22) and Krypton (24). The codename appears in the lts field of every release in the nodejs.org distribution index, which is a handy way to tell an LTS build from a Current one in automation.
The previous releases page on nodejs.org sums up the policy in one line: "Production applications should only use Active LTS or Maintenance LTS releases."
One rule matters for anyone planning upgrades. The README says that changes needed for critical security and bug fixes "may lead to semver-major changes landing within a Current or LTS release stream." Those cases are rare, they land as semver minor, and they should include a revert option. In practice this means a security release can change a default that your application relied on, and the release notes will tell you how to switch it back while you fix your code.
What changes from Node.js 27
In early 2026 the project announced a new schedule in a Node.js blog post titled "Evolving the Node.js Release Schedule" (the page shows it as last updated on 10 March 2026). The post explains that the old model was created during the io.js merger and that a decade of data showed that "odd-numbered releases see minimal adoption."
From October 2026:
- There is one major release per year, in April, with LTS promotion in October.
- Every release line becomes LTS. Node.js 27 will be the first odd numbered LTS.
- A new Alpha phase runs for six months, October to March, and allows semver major changes. Alpha builds are signed, tagged and tested, but the README says they are "not for production use."
- Version numbers align with the calendar year of the first Current release: 27.0.0 in 2027, 28.0.0 in 2028.
- Every line receives 30 months of LTS, for 36 months from first Current release to end of life, and lines stay in Active LTS for the whole 30 months instead of passing through a separate Maintenance phase.
According to schedule.json, Node.js 27 starts Alpha on 28 October 2026, becomes Current on 22 April 2027, enters LTS on 20 October 2027 and reaches end of life on 30 April 2030. Node.js 26 keeps the old model: Current from 5 May 2026, Active LTS from 28 October 2026, Maintenance from 20 October 2027, end of life on 30 April 2029.
Timeline: major versions and their security relevant changes
The table below lists every major line from the merge onward, with the first release date from the Release working group and the change in that line that matters most to security and supply chain teams. "Feature version" means the release where the feature first appeared; several features landed in a minor release of an earlier line too, as noted.
| Line | First release | LTS codename | Security relevant change |
|---|---|---|---|
| 0.10 | 2013-03-11 | none | Long lived pre merge line; no formal LTS policy |
| 0.12 | 2015-02-06 | none | Last 0.x line; end of life 2016-12-31 |
| io.js 1.0.0 | 2015-01-14 | none | Fork with open governance and fast V8 updates |
| 4.x | 2015-09-08 | Argon | Merge of Node.js and io.js; first LTS line |
| 6.x | 2016-04-26 | Boron | Second LTS; OpenSSL 1.0.2 |
| 8.x | 2017-05-30 | Carbon | Bundles npm 5.0.0, the first npm 5 release in a Node.js line |
| 10.x | 2018-04-24 | Dubnium | Moves to OpenSSL 1.1.0, later 1.1.1 |
| 11.x | 2018-10-23 | none | 11.8.0 adds experimental policy manifests ("module integrity checks through a manifest file") and an experimental process.report |
| 12.x | 2019-04-23 | Erbium | OpenSSL 1.1.1 and TLS 1.3; LTS split becomes 12 months active plus 18 maintenance |
| 14.x | 2020-04-21 | Fermium | Last line to bundle npm 6 |
| 16.x | 2021-04-20 | Gallium | End of life brought forward to 2023-09-11 to match OpenSSL 1.1.1 end of life |
| 17.x | 2021-10-19 | none | First line built on OpenSSL 3.0 |
| 18.x | 2022-04-19 | Hydrogen | First LTS on OpenSSL 3; global fetch on by default; node:test added |
| 19.x | 2022-10-18 | none | Single executable applications added in 19.7.0 |
| 20.x | 2023-04-18 | Iron | Experimental permission model; stable test runner |
| 21.x | 2023-10-17 | none | fetch marked stable |
| 22.x | 2024-04-24 | Jod | Policy manifests removed in 22.2.0; permission model stable in 22.13.0 |
| 23.x | 2024-10-16 | none | Permission model stable and flag renamed to --permission in 23.5.0 |
| 24.x | 2025-05-06 | Krypton | Later moves to OpenSSL 3.5; --permission-audit added in 24.20.0 |
| 25.x | 2025-10-15 | none | --allow-net network permission added in 25.0.0 |
| 26.x | 2026-05-05 | (LTS from 2026-10-28) | Last line on the even and odd model |
| 27.x | Alpha 2026-10-28 | (LTS from 2027-10-20) | First line under the yearly, every line LTS schedule |
A few of these deserve more detail.
OpenSSL 3. Node.js 17.0.0 shipped on 19 October 2021 with OpenSSL 3.0.0 (specifically the quictls fork, which adds QUIC support). The 17.0.0 release notes warned of "some ecosystem impact due to tightened restrictions on the allowed algorithms and key sizes." Applications or modules using an algorithm or key size no longer allowed by default saw an ERR_OSSL_EVP_UNSUPPORTED error, and the project added --openssl-legacy-provider as a temporary workaround. The same notes say OpenSSL 3.0 made FIPS support available again through the new FIPS module. Node.js 18 was the first LTS line to ship OpenSSL 3.
Node.js 16 is the cautionary tale. The project hoped to ship it with OpenSSL 3, but release timing did not allow it, so 16 went out on OpenSSL 1.1.1. Because OpenSSL 1.1.1 support ended on 11 September 2023, the Node.js project moved the Node.js 16 end of life forward by seven months to that same date, as Richard Lau's announcement on the Node.js blog explains. Teams that had planned around an April 2024 end of life lost seven months of runway.
Policy manifests. Node.js 11.8.0 added an experimental policy feature for "module integrity checks through a manifest file": you described in JSON which files could be loaded and which integrity hashes they had to match. It never left experimental status. The --experimental-policy flag was removed in Node.js 22.2.0 on 15 May 2024, and a separate documentation change removed mentions of the policy model from the security information. The permission model is a different mechanism with a different goal, so do not read it as a replacement.
The test runner, fetch and single executable applications. None of these is a security control on its own, but each one reduced the number of third party packages a typical project needs. node:test arrived in 18.0.0 and 16.17.0 and became stable in 20.0.0. Global fetch, built on the bundled undici library, was experimental in 17.5.0, enabled by default in 18.0.0 and marked stable in 21.0.0. Single executable applications arrived in 19.7.0 and 18.16.0 and are still documented at stability 1.1, active development. Each one is a bundled dependency you now inherit from the runtime instead of from node_modules, which changes who you wait on for a fix.
Release lines at a glance: dates, OpenSSL and security features
This table pairs each line's support window with the OpenSSL version recorded in the official nodejs.org distribution index, first release and latest release as of 25 October 2026. Dates come from schedule.json.
| Line | Start | End of life | OpenSSL at first release | OpenSSL in latest release | Notable security feature |
|---|---|---|---|---|---|
| 4.x Argon | 2015-09-08 | 2018-04-30 | 1.0.2d | 1.0.2o | First LTS line |
| 6.x Boron | 2016-04-26 | 2019-04-30 | 1.0.2g | 1.0.2r | LTS backports of security fixes |
| 8.x Carbon | 2017-05-30 | 2019-12-31 | 1.0.2k | 1.0.2s | npm lockfiles by default |
| 10.x Dubnium | 2018-04-24 | 2021-04-30 | 1.1.0h | 1.1.1k | OpenSSL 1.1.x |
| 12.x Erbium | 2019-04-23 | 2022-04-30 | 1.1.1b | 1.1.1n | TLS 1.3 |
| 14.x Fermium | 2020-04-21 | 2023-04-30 | 1.1.1f | 1.1.1t | Last LTS line on npm 6 |
| 16.x Gallium | 2021-04-20 | 2023-09-11 | 1.1.1k | 1.1.1v | Early end of life with OpenSSL 1.1.1 |
| 18.x Hydrogen | 2022-04-19 | 2025-04-30 | 3.0.2 | 3.0.16 | First LTS on OpenSSL 3 |
| 20.x Iron | 2023-04-18 | 2026-04-30 | 3.0.8 | 3.0.19 | Permission model (experimental) |
| 22.x Jod | 2024-04-24 | 2027-04-30 | 3.0.13 | 3.5.9 | Permission model stable from 22.13.0 |
| 24.x Krypton | 2025-05-06 | 2028-04-30 | 3.0.16 | 3.5.9 | Permission audit mode from 24.20.0 |
| 26.x | 2026-05-05 | 2029-04-30 | 3.5.5 | 3.5.9 | Last even and odd model line |
Two things stand out. First, LTS lines do move their bundled OpenSSL during their life: 22.x started on OpenSSL 3.0.13 and its latest release, 22.23.4 on 20 October 2026, carries 3.5.9. So "which OpenSSL does Node.js 22 use" has no single answer; it depends on the patch release. Second, an end of life line freezes whatever it had last. Node.js 20.20.2, the final 20.x release on 24 March 2026, carries OpenSSL 3.0.19, and nothing in that build will change again.
What does the Node.js permission model do, and what does it not do?
What it restricts
The permission model is enabled with the --permission flag (before 23.5.0 and 22.13.0 it was --experimental-permission). Once enabled, the docs say Node.js restricts:
- file system access through the
fsmodule - network access
- environment variables
- spawning child processes
node:worker_threads- native addons
- WASI
- FFI
- enabling the runtime inspector (the SIGUSR1 listener is not created)
You then grant back only what the program needs:
| Resource | Flag | Example |
|---|---|---|
| File reads | --allow-fs-read | --allow-fs-read=/srv/app/ |
| File writes | --allow-fs-write | --allow-fs-write=/tmp/uploads/ |
| Child processes | --allow-child-process | needed for child_process.spawn |
| Worker threads | --allow-worker | needed for new Worker() |
| Network | --allow-net | added in 25.0.0 |
| Environment variables | --allow-env | --allow-env=PORT --allow-env=APP_* |
| Native addons | --allow-addons | needed for .node binaries |
| WASI | --allow-wasi | needed for node:wasi |
| Inspector | --allow-inspector | debugger attach |
A denied operation throws ERR_ACCESS_DENIED with a permission field such as FileSystemRead and the resource it tried to reach. At runtime, process.permission.has(scope, reference) lets code check a grant, and process.permission.drop(scope, reference) drops one irreversibly, for example after reading a configuration file at startup.
The environment variable control works differently from the others. Instead of checking each read, Node.js removes every variable that --allow-env does not grant from the process environment at startup, before any JavaScript runs. Removed variables vanish from process.env, diagnostic reports, native getenv() calls, worker threads and the environment inherited by child processes.
Newer releases also add an audit mode. With --permission-audit (added in 25.8.0 and 24.20.0) every check still runs, but violations are published on node:diagnostics_channel channels such as node:permission-model:fs instead of being denied. The docs position it as a way to discover what permissions an application needs before switching to enforce mode.
What it does not do
The documentation is unusually direct about the limits:
- "This feature does not protect against malicious code." The model is described as a "seat belt" that prevents trusted code from unintentionally changing files or using resources it was not granted. The same page says malicious code "can bypass the permission model."
- The file system restriction covers the
fsmodule. The docs say it "does not guarantee" that code cannot reach the file system by other means, namingnode:sqliteas an example. - Dropping a permission only affects future checks. Open file descriptors, sockets, child processes and workers stay open.
- A small set of
fsoperations act on an open descriptor rather than a path (fsync,fdatasync,fchmod,fchown,futimesand their synchronous forms). With the model enabled these are simply disabled. --allow-openssl-storegrants broad authority to OpenSSL STORE loaders, which the docs say "may access files, devices, tokens, or the network" outside thefsandnetscopes.
The security policy in the Node.js repository repeats the point and spells out how reports are triaged. The permission model "is designed to reduce the blast radius of mistakes in trusted application code, not to act as a security boundary against intentional misuse or a compromised process." Reports are sorted into three lanes: a vulnerability (something Node.js does not trust crosses a permission check without trusted code already running in the process), a security interest bug (Node.js enforces a documented invariant inconsistently), and excluded cases such as intentional misuse by code already in the process.
The worker thread note is the one teams most often miss. Creating a worker requires --allow-worker, but a worker started with a modified execArgv or env "may start without inheriting the parent's permission configuration." The policy's conclusion: "Applications that rely on the Permission Model must not grant --allow-worker to code they do not trust."
When to use it
Use the permission model the way you would use a read only root file system or a narrow service account: as a layer that turns accidents into loud errors. It is useful for build scripts, CLI tools, scheduled jobs and services that only need a few directories. It does not replace operating system isolation. If you run code you do not trust, including a dependency you have not reviewed, the Node.js project's own advice for its virtual file system applies here as well: use OS level isolation, such as separate users, containers or platform sandboxes, when a security boundary is required.
How are Node.js security releases run?
The disclosure process
The Node.js SECURITY.md lays out the process at a level any team can follow:
- Reports go to the project's HackerOne program. The project normally acknowledges within 5 days and sends a more detailed response within 10 days. If a reporter hears nothing within 6 business days, they may escalate to the OpenJS Foundation CNA.
- A primary handler validates the report against all supported Node.js versions, determines every affected version and audits the code for similar problems.
- Fixes are prepared for all supported release lines but are not committed to the public repository. They are held locally until the announcement.
- An embargo date is chosen and a CVE is requested. The embargo is typically 72 hours from the time the CVE is issued, though it varies with severity and the difficulty of the fix.
- On the embargo date, the announcement goes to the Node.js security mailing list, the changes are pushed publicly, and new builds are deployed to nodejs.org. Within 6 hours, the advisory is published on the Node.js blog.
The project's contributor guide for security releases adds the operational detail. One week before the planned release, a pre release blog post goes up on nodejs.org and notifications go out: to the nodejs-sec Google Group, the oss-security mailing list, and issue trackers for the official Docker images and the build working group. The template for that pre alert names the release lines that will be updated, the planned date, and how many high and moderate severity issues are being addressed, without technical detail. The guide also asks collaborators other than the releasers and the security steward not to publicise the release early. Then the releasers cut patch releases for every affected supported line on or shortly after the announced date. If a security release contains only an OpenSSL update, the guide includes extra wording for the pre announcement to say so. That is why Node.js security releases arrive as a batch, for example a 22.x, 24.x and 26.x release published together, each flagged as a security release in the distribution index.
SECURITY.md also states that the Node.js project no longer has a bug bounty program, and the Node.js blog carries a post titled "Security Bug Bounty Program Paused Due to Loss of Funding." Reports still go through HackerOne; only the reward has gone.
The threat model, in plain terms
The threat model in SECURITY.md decides what counts as a Node.js vulnerability and what does not. It is worth reading before you file a report or argue with a scanner.
Node.js does not trust:
- data from the remote end of inbound connections that Node.js parses, such as HTTP server traffic
- data from outbound connections that Node.js parses (HTTP client and DNS), except with respect to payload length
- consumers of data protected by Node.js crypto APIs
- file content and other I/O opened through Node.js APIs
Node.js trusts everything else, including:
- the developers and infrastructure that run it, and the operating system
- the code it is asked to run, "including JavaScript, WASM and native code," explicitly including "all dependencies installed from the npm registry"
- inputs that application code passes to Node.js APIs
- the file system when requiring a module
- any inspector connection
This has direct consequences. A malicious npm package is out of scope for Node.js itself ("Code is trusted by Node.js"). Prototype pollution that depends on unsanitised application input is out of scope. Path traversal caused by passing user input straight to fs is an application bug. HTTP request smuggling in the built in parser, by contrast, is a textbook Node.js vulnerability, and so is incorrect certificate name validation.
The same document excludes from CVE treatment features behind --experimental-* flags at stability 1.0 or 1.1, features behind compile time or V8 flags, and issues that only affect experimental platforms. It also names some newer subsystems explicitly: the experimental QUIC and HTTP/3 stack is not considered in scope "at this time," and the experimental virtual file system is "not a sandbox, permission system, or security boundary for untrusted code."
How to follow security releases
The practical channels are the Node.js vulnerability blog at nodejs.org/en/blog/vulnerability, the security flag on each entry in https://nodejs.org/dist/index.json, and the nodejs-sec mailing list referenced in the policy. The Node.js Security Team documents its initiatives in the nodejs/security-wg repository, whose README is explicit that the team is "not responsible for managing or responding to security reports against Node.js itself"; that responsibility stays with the Technical Steering Committee. For a history of incidents that happened in the npm registry rather than in the runtime, see our npm incident history from left-pad to Shai-Hulud.
Bundled dependencies: why one Node.js version is really many components
What ships inside the binary
A Node.js release is not just the code in lib/ and src/. The project vendors a long list of third party libraries in its deps/ directory, and the contributor guide "Maintaining dependencies" lists each one with the process used to update it. The security relevant ones include:
| Bundled component | What it does in Node.js | Why it matters |
|---|---|---|
| OpenSSL | TLS, crypto, certificate validation | Most Node.js security releases that touch crypto follow an OpenSSL advisory |
| V8 | JavaScript engine | Engine fixes arrive through V8 updates backported by the project |
| llhttp | HTTP/1.1 parser behind http | Request smuggling fixes land here |
| undici | HTTP client behind global fetch | Client side HTTP fixes land here, and undici also ships separately on npm |
| nghttp2 | HTTP/2 | Protocol level denial of service fixes |
| c-ares | DNS resolution | Resolver fixes |
| libuv | Event loop, file system, networking | Platform level I/O fixes |
| zlib | Compression | Decompression fixes |
| ICU | Internationalisation | Locale data and parsing |
| npm | Package manager CLI | Ships with Node.js but has its own release cycle |
You can see the exact versions your runtime carries with one command:
node -p "process.versions"
The output lists node, v8, uv, zlib, ares, nghttp2, llhttp, openssl, undici, icu and more. The same data is published for every official release in https://nodejs.org/dist/index.json, which records npm, v8, uv, zlib, openssl and a security flag per version.
npm is bundled, but it is not the runtime
The npm CLI ships in every Node.js installer and official image, but its fixes follow npm's release cycle. The version inside a Node.js release only changes when the Node.js project updates it in a later release. Node.js 22.23.4 bundles npm 10.9.9 while Node.js 24.22.1 bundles npm 11.19.1, both released on 20 October 2026. If an npm CLI advisory appears, updating Node.js may or may not pull the fixed npm, and running npm install -g npm@<version> is a separate decision that changes your image again. Corepack is a related case. SECURITY.md says Corepack downloads the latest or requested version of a package manager, so "Node.js releases won't be affected" by vulnerabilities in that software, and "users are responsible for keeping the software they use through Corepack up-to-date."
Why this matters for SBOMs and scanners
Three practical problems follow from bundling.
- A scanner that only reads
package-lock.jsonwill not see the runtime. Your lockfile lists what npm installed intonode_modules. It does not list OpenSSL, llhttp or undici inside thenodebinary. Unless something records the Node.js version itself, an advisory against Node.js 22.x will not match anything in that SBOM. - The same library can appear twice. If your application depends on the
undicipackage from npm, you have one undici version innode_modulesand another compiled into the runtime behindfetch. They are patched separately. - Distribution builds can differ. Linux distributions sometimes build Node.js against the system OpenSSL instead of the bundled one.
process.versions.openssltells you what your process actually loaded; the nodejs.org index only describes the official binaries.
The fix is to record the runtime as a component in its own right: the Node.js version in each container image, each .nvmrc and each CI job, so an advisory against Node.js itself can be matched to the places it runs.
Step by step: inventory Node.js versions and plan an LTS upgrade
This walkthrough assumes a Unix shell with git, jq and curl. Every command reads files; none changes your repositories.
Step 1: find declared versions in each repository
# From the root of a checkout: list files that pin or constrain Node.js
git ls-files | grep -E '(^|/)(\.nvmrc|\.node-version|\.tool-versions|package\.json|Dockerfile[^/]*|\.github/workflows/[^/]+\.ya?ml)$'
# Version files used by version managers
cat .nvmrc .node-version 2>/dev/null
grep -E '^nodejs ' .tool-versions 2>/dev/null
# The engines field in package.json
jq -r '.engines.node // "engines.node not set"' package.json
# Base images in Dockerfiles
grep -RnE --include='Dockerfile*' '^FROM[[:space:]]+node:' .
# setup-node versions in GitHub Actions workflows
grep -RnE 'node-version' .github/workflows 2>/dev/null
Run the same commands across all repositories with a loop over your checkouts, and write the results to a CSV with columns for repository, file, declared version and type (nvmrc, engines, Dockerfile, CI).
Step 2: confirm the runtime that actually runs
Declared versions drift from reality. Check the binaries:
# On a host or in a CI job
node -p "process.version + ' openssl ' + process.versions.openssl"
# Inside a built image, without starting the app
docker run --rm --entrypoint node your-registry/your-app:tag -p "process.version"
# Full list of bundled component versions
docker run --rm --entrypoint node your-registry/your-app:tag -p "JSON.stringify(process.versions, null, 2)"
For images you have already shipped, record the digest as well as the tag. A tag like node:22 moves; the digest does not.
Step 3: map each version to its support status
The Release working group publishes the schedule as JSON. This prints every line that has not reached end of life today:
curl -s https://raw.githubusercontent.com/nodejs/Release/main/schedule.json \
| jq -r --arg today "$(date +%F)" \
'to_entries[] | select(.value.end >= $today)
| "\(.key)\tstart=\(.value.start)\tlts=\(.value.lts // "none")\tmaintenance=\(.value.maintenance // "none")\tend=\(.value.end)"'
And this shows the newest release of a line with its bundled npm and OpenSSL, plus whether the newest release was a security release:
curl -s https://nodejs.org/dist/index.json \
| jq -r '[.[] | select(.version | startswith("v22."))][0]
| "\(.version) \(.date) npm=\(.npm) openssl=\(.openssl) security=\(.security)"'
Compare each runtime from Step 2 against two questions: is the line still supported, and is this the newest patch release in the line? A supported line on an old patch release is still missing security fixes.
Step 4: pick the target line
| Current runtime | Status on 25 Oct 2026 | Suggested target |
|---|---|---|
| 18.x or older | End of life | 24.x now, or 26.x once your tests pass on it |
| 20.x | End of life since 2026-04-30 | 24.x |
| 21.x, 23.x, 25.x | End of life (odd lines) | 24.x |
| 22.x | Maintenance LTS until 2027-04-30 | Plan the move to 24.x or 26.x before April 2027 |
| 24.x | Maintenance LTS until 2028-04-30 | Stay current on patches; schedule 26.x |
| 26.x | Current, LTS from 2026-10-28 | Production ready once it enters LTS |
| 27.x Alpha | Alpha from 2026-10-28 | CI only, not production |
Step 5: test the upgrade
# Switch locally with a version manager
nvm install 24
nvm use 24
node -v
# Reinstall from the lockfile so native addons rebuild for the new ABI
rm -rf node_modules
npm ci
# Surface deprecations as failures while you test
NODE_OPTIONS="--pending-deprecation --throw-deprecation" npm test
Native addons are compiled against a specific Node.js ABI, reported by node -p process.versions.modules. npm ci on the new runtime rebuilds or downloads matching binaries; prebuilt binaries for very new lines sometimes lag behind.
In CI, run the old and new lines side by side until the new one is green:
# .github/workflows/test.yml
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22, 24, 26]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm test
Step 6: update every declaration in one change
Change .nvmrc, the engines field, the Dockerfile base image and the CI matrix together, so the next person does not find three different answers. Pin the base image by digest and let a dependency update bot or your own pipeline propose digest bumps:
# Example: pin the line and the exact image
FROM node:24-bookworm-slim@sha256:<digest-from-your-registry>
Then rebuild every image that uses it. Updating the base image does nothing for containers already running; they need a redeploy.
Step 7: try the permission model in audit mode
If you are on 24.20.0 or later, or 25.8.0 or later, run your test suite or a staging instance with audit mode to see what the application really touches:
node --permission-audit --allow-fs-read=/srv/app/ server.js
Subscribe to the node:permission-model:* diagnostics channels in a small preload script to log each would be denial, then turn the observed set into explicit --allow-* flags and switch to --permission. Remember the earlier limits: this protects against your own mistakes, not against a hostile dependency.
Common mistakes
Running odd numbered releases in production. Through Node.js 25, odd lines were supported for about eight months and never received LTS. Teams that deployed 21.x, 23.x or 25.x often found themselves on an end of life runtime within a year. From Node.js 27 every line becomes LTS, but Alpha builds are explicitly not for production.
Pinning a base image for years. A Dockerfile that says FROM node:16 or pins an old digest keeps the runtime, OpenSSL and npm from the day the image was built. The line itself may have reached end of life long ago. Pin by digest for reproducibility, but schedule the digest updates.
Assuming an npm fix arrives with a Node.js patch. The bundled npm only changes when the Node.js project updates it in a release. An npm advisory may need its own action.
Assuming a Node.js patch fixes your node_modules. The reverse also happens. Updating Node.js changes the bundled undici behind fetch, but not the undici package your code imports from npm.
Trusting the permission model as isolation. The docs say it does not protect against malicious code. Granting --allow-worker or --allow-child-process to code you do not trust removes most of the value.
Reading only the lockfile for vulnerabilities. A lockfile based SBOM does not include the Node.js runtime or its bundled libraries. Record the runtime version per image and per host.
Ignoring the semver minor security exception. A security release can change a default within an LTS line. Read the release notes and test before rolling a security patch to every service at once, but do roll it.
Treating a supported line as patched. Being on 22.x is not the same as being on the latest 22.x. Check the patch release against the newest one in index.json.
FAQ
When was Node.js first released?
In 2009. The project's original 0.x ChangeLog lists version 0.0.3 on 11 June 2009 and 0.1.0 on 30 June 2009. Releases carried 0.x version numbers until 4.0.0 in September 2015, when the Node.js and io.js codebases merged.
What was io.js and why did it exist?
io.js was a fork of Node.js that ran as a separate project with its own fast moving version numbers. It shipped io.js 1.0.0 on 14 January 2015 and its last release, 3.3.1, on 15 September 2015, after the projects merged as Node.js 4.0.0.
Why is there no Node.js 1, 2 or 3?
io.js had already used versions 1.x to 3.x. The Node.js Foundation's announcement says the merged release was "named version 4.0.0 because it includes major updates from io.js version 3.0.0."
How long is a Node.js LTS release supported?
For lines from Node.js 12 to 26, 30 months of LTS: 12 months Active LTS then 18 months Maintenance, after about six months as Current. From Node.js 27, lines get 30 months of LTS and 36 months in total from the first Current release. Our Node.js LTS support window post has the current dates.
Which Node.js versions are supported right now?
On 25 October 2026: 22.x (Maintenance LTS, end of life 30 April 2027), 24.x (Maintenance LTS, end of life 30 April 2028) and 26.x (Current, Active LTS from 28 October 2026, end of life 30 April 2029). Node.js 27 starts as an Alpha on 28 October 2026 for testing only.
Is the Node.js permission model a sandbox?
No. The documentation says it "does not protect against malicious code" and calls it a seat belt for trusted code. Use operating system isolation such as separate users or containers when you need a security boundary.
What does --permission block by default?
File system access through fs, network access, environment variables, child processes, worker threads, native addons, WASI, FFI and the inspector. Each is granted back with an --allow-* flag.
What happened to Node.js policy manifests?
The experimental policy feature, added in 11.8.0 to check module integrity against a JSON manifest, never became stable and was removed in Node.js 22.
Why does a Node.js security release update several versions at once?
Fixes are prepared privately for every supported line and released on the same embargo date, after a pre announcement on the Node.js blog. Each affected line gets its own patch release, published together.
Does updating Node.js update npm?
Only to the npm version bundled in that Node.js release. npm has its own release cycle, so an npm advisory may require updating npm separately.
Why supply chain hygiene still matters for your Node.js runtime
Every upgrade in the walkthrough above ends with npm ci, which pulls your whole dependency tree again, and every image rebuild picks up a new runtime with new bundled libraries. Knowing which versions you ship, and having a fast way to move them to fixed versions, is the part of this problem that sits in your own pipeline rather than with the Node.js release team.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.
3. Gold Registry: start from components that are already fixed
Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
The Node.js runtime itself is patched by the Node.js project; Safeguard does not ship Node.js builds. Where it helps is the inventory and the dependency tree around the runtime: scanning repositories, container images and SBOM files, showing which projects contain a given component and version, and gating what ships.
The first demo is a real Safeguard CLI session. You ask it to fix a lodash CVE in a repository, it reads the finding and the advisory, edits package.json, and asks before any command that changes something, such as npm install and npm test. The same flow applies after a Node.js upgrade, when npm ci on a new line surfaces dependencies that need moving.
Click through it at your own pace, or open it full screen.
The second demo sets up the local runner on your own machine, approves it with a device code, and scans from the terminal. That is the route for running safeguard scan --dir . across the checkouts you inventoried in Step 1, or safeguard scan --image against the images from Step 2, with results written as JSON you can join to your inventory.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. Once connected, you can ask which projects contain a particular component and version, which is the question you need answered on the day a Node.js or npm advisory lands.
Click through it at your own pace, or open it full screen.
To try it on your own projects, download Safeguard or read the CLI docs.
Quick check
Which Node.js release combined the Node.js and io.js codebases?
Node.js 4.0.0, released in September 2015 and announced by the Node.js Foundation on 14 September 2015. It was numbered 4 because it built on io.js 3.0.0.
Does node --permission protect you from a malicious npm dependency?
No. The Node.js docs say the permission model does not protect against malicious code. It limits what trusted code can reach by accident; use operating system isolation for code you do not trust.
Sources
- Node.js Release working group README
- Node.js release schedule JSON
- Node.js distribution index
- io.js distribution index
- Node.js previous releases
- Node.js end of life
- Node.js 0.x ChangeLog (node-v0.x-archive)
- Node.js Foundation combines Node.js and io.js into single codebase in new release
- Linux Foundation: Node.js Foundation and JS Foundation merge to form OpenJS Foundation
- Bringing forward the end of life date for Node.js 16
- Node.js 11 changelog
- Node.js 12 changelog
- Node.js 17 changelog
- Node.js 22 changelog
- Node.js 24 changelog
- Node.js test runner documentation
- Node.js single executable applications documentation
- Node.js globals documentation (fetch)
- Evolving the Node.js release schedule
- Node.js permissions documentation
- Node.js command line options
- Node.js SECURITY.md
- Node.js security release process
- Node.js maintaining dependencies guide
- Node.js vulnerability blog
- Node.js Security Working Group
- OpenJS Foundation
Last reviewed: 25 October 2026. Release dates are subject to change; check the Node.js Release repository for the current schedule.