Safeguard
Supply Chain Security•

Node.js Release History: io.js, LTS Lines, Permissions, Security

How Node.js went from 0.x and the io.js fork to 30 month LTS lines, what the permission model really restricts, and how security releases ship.

Safeguard Security Team
40 min read

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:

PhaseWhat landsWho should run it
CurrentMost non breaking changes from mainLibrary authors, early adopters, CI testing
Active LTSFeatures, fixes and updates audited by the Release team as stable for the lineProduction
Maintenance LTSCritical bug fixes and security updates; features only when they help migrationProduction that has not yet moved
End of lifeNothing, including security fixesNobody

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.

LineFirst releaseLTS codenameSecurity relevant change
0.102013-03-11noneLong lived pre merge line; no formal LTS policy
0.122015-02-06noneLast 0.x line; end of life 2016-12-31
io.js 1.0.02015-01-14noneFork with open governance and fast V8 updates
4.x2015-09-08ArgonMerge of Node.js and io.js; first LTS line
6.x2016-04-26BoronSecond LTS; OpenSSL 1.0.2
8.x2017-05-30CarbonBundles npm 5.0.0, the first npm 5 release in a Node.js line
10.x2018-04-24DubniumMoves to OpenSSL 1.1.0, later 1.1.1
11.x2018-10-23none11.8.0 adds experimental policy manifests ("module integrity checks through a manifest file") and an experimental process.report
12.x2019-04-23ErbiumOpenSSL 1.1.1 and TLS 1.3; LTS split becomes 12 months active plus 18 maintenance
14.x2020-04-21FermiumLast line to bundle npm 6
16.x2021-04-20GalliumEnd of life brought forward to 2023-09-11 to match OpenSSL 1.1.1 end of life
17.x2021-10-19noneFirst line built on OpenSSL 3.0
18.x2022-04-19HydrogenFirst LTS on OpenSSL 3; global fetch on by default; node:test added
19.x2022-10-18noneSingle executable applications added in 19.7.0
20.x2023-04-18IronExperimental permission model; stable test runner
21.x2023-10-17nonefetch marked stable
22.x2024-04-24JodPolicy manifests removed in 22.2.0; permission model stable in 22.13.0
23.x2024-10-16nonePermission model stable and flag renamed to --permission in 23.5.0
24.x2025-05-06KryptonLater moves to OpenSSL 3.5; --permission-audit added in 24.20.0
25.x2025-10-15none--allow-net network permission added in 25.0.0
26.x2026-05-05(LTS from 2026-10-28)Last line on the even and odd model
27.xAlpha 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.

LineStartEnd of lifeOpenSSL at first releaseOpenSSL in latest releaseNotable security feature
4.x Argon2015-09-082018-04-301.0.2d1.0.2oFirst LTS line
6.x Boron2016-04-262019-04-301.0.2g1.0.2rLTS backports of security fixes
8.x Carbon2017-05-302019-12-311.0.2k1.0.2snpm lockfiles by default
10.x Dubnium2018-04-242021-04-301.1.0h1.1.1kOpenSSL 1.1.x
12.x Erbium2019-04-232022-04-301.1.1b1.1.1nTLS 1.3
14.x Fermium2020-04-212023-04-301.1.1f1.1.1tLast LTS line on npm 6
16.x Gallium2021-04-202023-09-111.1.1k1.1.1vEarly end of life with OpenSSL 1.1.1
18.x Hydrogen2022-04-192025-04-303.0.23.0.16First LTS on OpenSSL 3
20.x Iron2023-04-182026-04-303.0.83.0.19Permission model (experimental)
22.x Jod2024-04-242027-04-303.0.133.5.9Permission model stable from 22.13.0
24.x Krypton2025-05-062028-04-303.0.163.5.9Permission audit mode from 24.20.0
26.x2026-05-052029-04-303.5.53.5.9Last 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 fs module
  • 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:

ResourceFlagExample
File reads--allow-fs-read--allow-fs-read=/srv/app/
File writes--allow-fs-write--allow-fs-write=/tmp/uploads/
Child processes--allow-child-processneeded for child_process.spawn
Worker threads--allow-workerneeded for new Worker()
Network--allow-netadded in 25.0.0
Environment variables--allow-env--allow-env=PORT --allow-env=APP_*
Native addons--allow-addonsneeded for .node binaries
WASI--allow-wasineeded for node:wasi
Inspector--allow-inspectordebugger 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 fs module. The docs say it "does not guarantee" that code cannot reach the file system by other means, naming node:sqlite as an example.
  • Dropping a permission only affects future checks. Open file descriptors, sockets, child processes and workers stay open.
  • A small set of fs operations act on an open descriptor rather than a path (fsync, fdatasync, fchmod, fchown, futimes and their synchronous forms). With the model enabled these are simply disabled.
  • --allow-openssl-store grants broad authority to OpenSSL STORE loaders, which the docs say "may access files, devices, tokens, or the network" outside the fs and net scopes.

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:

  1. 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.
  2. A primary handler validates the report against all supported Node.js versions, determines every affected version and audits the code for similar problems.
  3. Fixes are prepared for all supported release lines but are not committed to the public repository. They are held locally until the announcement.
  4. 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.
  5. 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 componentWhat it does in Node.jsWhy it matters
OpenSSLTLS, crypto, certificate validationMost Node.js security releases that touch crypto follow an OpenSSL advisory
V8JavaScript engineEngine fixes arrive through V8 updates backported by the project
llhttpHTTP/1.1 parser behind httpRequest smuggling fixes land here
undiciHTTP client behind global fetchClient side HTTP fixes land here, and undici also ships separately on npm
nghttp2HTTP/2Protocol level denial of service fixes
c-aresDNS resolutionResolver fixes
libuvEvent loop, file system, networkingPlatform level I/O fixes
zlibCompressionDecompression fixes
ICUInternationalisationLocale data and parsing
npmPackage manager CLIShips 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.

  1. A scanner that only reads package-lock.json will not see the runtime. Your lockfile lists what npm installed into node_modules. It does not list OpenSSL, llhttp or undici inside the node binary. Unless something records the Node.js version itself, an advisory against Node.js 22.x will not match anything in that SBOM.
  2. The same library can appear twice. If your application depends on the undici package from npm, you have one undici version in node_modules and another compiled into the runtime behind fetch. They are patched separately.
  3. Distribution builds can differ. Linux distributions sometimes build Node.js against the system OpenSSL instead of the bundled one. process.versions.openssl tells 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 runtimeStatus on 25 Oct 2026Suggested target
18.x or olderEnd of life24.x now, or 26.x once your tests pass on it
20.xEnd of life since 2026-04-3024.x
21.x, 23.x, 25.xEnd of life (odd lines)24.x
22.xMaintenance LTS until 2027-04-30Plan the move to 24.x or 26.x before April 2027
24.xMaintenance LTS until 2028-04-30Stay current on patches; schedule 26.x
26.xCurrent, LTS from 2026-10-28Production ready once it enters LTS
27.x AlphaAlpha from 2026-10-28CI 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 situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingSafeguard policies and gates in CI
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch
You want to start clean on new projectsPull 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

Last reviewed: 25 October 2026. Release dates are subject to change; check the Node.js Release repository for the current schedule.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.