Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Node.js 20.11.1: The Quiet Patch Every LTS Team Should Actually Care About

Sean

Platform Writer

Jun 17, 2026
9 min read

Node.js 20.11.1 is a security release for the Iron LTS line. It ships fixes for four High-severity CVEs and four Medium-severity CVEs, plus an undici bump to 5.28.3 and OpenSSL to 3.0.13+quic1, and it disables io_uring by default to plug a privilege escalation. Released on 2024-02-14. That is the short version. The longer version is that 20.11.1 is the kind of patch that makes a difference between a normal quarter and a CVE-driven Sunday afternoon, and it is the version any team pinning to 20.x should land on before the next incident report forces the issue.

This post is a working engineer’s take on what 20.11.1 actually changes, why each fix matters in production, and how to upgrade without dragging a real customer-visible outage behind you. It assumes you are running 20.x in some form (a Dockerfile base image, an nvm-pinned CI matrix, a PaaS that bakes its own node), that you have been bitten by a security patch before, and that you would rather skip the postmortem this time.

Node.js 20.11.1: The quiet patch every LTS team should actually care about

Table of contents

What actually shipped in 20.11.1

The release notes are unusually short. There are no “new features” sections, no deprecations, no API changes. There are ten commit entries, all of them CVE fixes or dependency bumps. That is a security release. Read it like one.

The headline changes:

  • Four High CVEs: CVE-2024-21892 (Linux capabilities code injection), CVE-2024-22019 (HTTP chunk extension DoS), CVE-2024-21896 (Buffer path traversal), CVE-2024-22017 (setuid not dropping privileges when io_uring is enabled).
  • Four Medium CVEs: CVE-2023-46809 (Marvin Attack against PKCS#1 v1.5 padding), CVE-2024-21891 (permission model path traversal bypasses), CVE-2024-21890 (--allow-fs-read and --allow-fs-write wildcards), CVE-2024-22025 (fetch() brotli DoS).
  • io_uring disabled by default in libuv, to close the privilege escalation path.
  • undici bumped to 5.28.3, which is the HTTP client most modern node apps use indirectly.
  • OpenSSL bumped to 3.0.13+quic1, including the Marvin Attack mitigation.

A security release that touches the HTTP parser, the buffer internals, the permission model, the cryptography layer, and the system-call surface is not a small release. It is just packaged as one.

Why a 0.0.1 bump is the loudest release of the year

A .0.1 increment on an LTS line looks like noise. It is the opposite of noise. The semver contract for node’s LTS is that the minor and major versions are reserved for breaking changes and feature changes, and the patch versions are reserved for security and bug fixes. A patch that ships ten commits and four High CVEs is the loudest release the LTS line will produce that year.

The reason this matters is the upgrade path. A 20.10 to 20.11.1 upgrade is, by design, drop-in. There are no API changes. There are no behavior changes beyond the security fixes themselves. If you were running 20.10 yesterday, the cost of running 20.11.1 tomorrow should be zero, and the cost of not running it grows every day the deploys stay on the old version.

The same is not true of 20.11.0. That was a feature release. It introduced changes worth reviewing. The .1 patch is the safe one to adopt on a Friday afternoon — which is the joke nobody wants to make out loud because too many teams have actually done it.

The four High CVEs, in plain English

CVE-2024-21892 — Linux capabilities code injection and privilege escalation (High). A flaw in how node handled Linux capabilities meant a properly crafted capability set could let unprivileged code execute outside its intended sandbox. For most apps this is theoretical. For any container that runs with a capability set smaller than root, or any app that processes untrusted input on a multi-tenant host, this is the kind of CVE that gets the k8s security scanner to start yelling. The fix is in the capabilities handling.

CVE-2024-22019 — HTTP chunk extension DoS (High). The HTTP parser accepted chunk extensions of unbounded size. A client could send a request with a chunk extension large enough to consume memory but small enough to keep the connection alive. The fix adds a maximum chunk extension size. The practical effect: an attacker cannot pin a node process to its memory ceiling with a single request anymore.

CVE-2024-21896 — Buffer path traversal (High). The kind of CVE that reads like a textbook entry. A path traversal flaw in the Buffer internals allowed code that had monkey-patched the Buffer constructor to read outside the intended path. The fix protects against modified Buffer internals in possiblyTransformPath. The practical effect: a transitive dependency that monkey-patches Buffer (yes, this happens) can no longer escape its sandbox through the path layer.

CVE-2024-22017 — setuid does not drop privileges with io_uring (High). When io_uring was enabled and a process called setuid, the privilege drop did not propagate through the io_uring path. The fix is to disable io_uring by default. The practical effect: a process that calls setuid to drop root can now trust that it has actually dropped root.

The Medium CVEs that still matter

CVE-2023-46809 — Marvin Attack (Medium). A timing attack against PKCS#1 v1.5 padding in RSA private decryption. The classic Bleichenbacher attack, refined. Only matters if you are decrypting RSA ciphertext from untrusted sources. The fix disables PKCS#1 padding for privateDecrypt. If you have code that does crypto.privateDecrypt with PKCS#1 padding, this release will change behavior for you. Audit your code first.

CVE-2024-21891 — Multiple permission model bypasses via path traversal sequences (Medium). The permission model introduced in 20.x added --allow-fs-read and --allow-fs-write flags to lock a process to a set of paths. The path traversal sequences were not fully sanitized, which meant clever relative paths could escape the allowlist. The fix tightens the path validation. If you use the permission model in production, this is the kind of patch that matters.

CVE-2024-21890 — Improper wildcard handling in --allow-fs-read and --allow-fs-write (Medium). Wildcards in the permission model’s allowlist did not always match what users expected. A pattern like --allow-fs-read=* matched more than it should have. The fix corrects the wildcard semantics. Audit any process startup script that passes wildcard flags.

CVE-2024-22025 — fetch() brotli DoS (Medium). The built-in fetch() (backed by undici) would accept brotli-encoded responses with unbounded decode buffer. A malicious server could pin the decoder to its memory ceiling. The fix adds a buffer limit. The practical effect: a server you fetch from can no longer pin your process with a single response.

Why io_uring had to go

io_uring is the Linux kernel’s high-performance async I/O interface. Node’s libuv had experimental support for it, because for the right workload it is significantly faster than the epoll-based path. The CVE-2024-22017 fix revealed that the privilege model around io_uring was not as airtight as it needed to be — a setuid call did not always propagate through the io_uring path.

The fix is to disable io_uring by default. Node still supports it behind an opt-in flag. For most apps, this is invisible. For the small set of apps that explicitly opted in to io_uring for throughput, the upgrade requires either re-enabling it deliberately or accepting the throughput hit. The safer answer is to leave it off until the kernel and libuv story stabilizes.

This is also a useful reminder that “experimental” features are experimental for a reason. The cost of running them in production is the cost of being on the bleeding edge of the security model.

How to tell if you are on 20.11.1 (or close)

The most reliable way:

node -p "process.versions.node"
# expected: v20.11.1

For a Docker image:

docker run --rm node:20.11.1 node -p "process.versions.node"

For a deployed service, hit the version endpoint you hopefully log on startup, or check your platform’s runtime inventory. On RunxBuild, the runtime version is recorded in the service configuration; bumping it is a deploy, not a code change.

A subtle one: some container registries tag the image as node:20.11.1 while bundling a different patch level due to base-image rebuilds. Always re-check node -p "process.versions.node" against what you think you are running. The number on the tag is the number that should match the running process.

The upgrade that does not break your weekend

The upgrade recipe for a service that already runs on 20.x:

  1. Confirm the current version. node -p "process.versions.node". If it is already 20.11.1 or later, you are done.
  2. Bump the Dockerfile. Change FROM node:20.10-bullseye-slim to FROM node:20.11.1-bullseye-slim (or whatever base variant you use). Pin the patch version, not just the minor.
  3. Rebuild locally. docker build -t myapp:test . and run the test suite. There should be no test failures from a security patch.
  4. Smoke test the running container. docker run --rm myapp:test node -p "process.versions.node" to confirm the new version is the one that ships, and run the app’s smoke suite.
  5. Deploy to staging first. A PaaS like RunxBuild will pull the new Dockerfile, rebuild, and run health checks. The deploy is a single command; the rollback is a single click.
  6. Watch the error rate and p99 latency for 30 minutes. A security patch should be invisible. If anything moves, roll back and investigate.
  7. Promote to production. Same flow, same health checks, same rollback path.

The reason this works is the same reason the patch exists: the semver contract for an LTS patch is “no behavior changes except the fixes.” A team that has been on 20.x for a quarter will see no difference in their app’s behavior. The only thing that changes is the CVE count.

The Dockerfile fix that costs nothing

The single highest-leverage change in a Dockerfile is to pin the patch version of the base image:

# Before — broad pin
FROM node:20-bullseye-slim

# After — patch pin
FROM node:20.11.1-bullseye-slim

The broad pin is convenient: you get every new patch on the next build. The patch pin is safer: a rebuild does not silently pick up a patch that has not been reviewed. The cost is one number to bump. The benefit is a deterministic build.

For a CI matrix, the equivalent is to pin the version in .nvmrc:

20.11.1

The .nvmrc file is read by nvm, by most CI runners, and by any editor with the node version plugin. A team that checks .nvmrc into the repo has the same answer for “what version is this code supposed to run on?” regardless of who is asking.

If your platform reads the node version from a config file (most do), pin it there too. On RunxBuild, the runtime version is configured per service, and a bumped value is a deploy — no Dockerfile required.

What 20.11.1 does not fix

A useful exercise: list the things this release does not address.

  • The upgrade treadmill on 20.x. 20.x is LTS until April 2026 for active support and October 2026 for maintenance. The clock is running. Teams that started on 20.x in early 2024 are halfway through the LTS window.
  • Transitive dependencies. The undici bump helps, but any dependency that bundles its own copy of a vulnerable library is still vulnerable until that dependency updates.
  • Application-level authn/authz. None of these CVEs touch the application layer. A node service with bad input validation is still bad input validation after the patch.
  • Third-party HTTP clients. The fetch fix is for the built-in client. axios, node-fetch, got, and any custom HTTP wrapper have their own vulnerability surface.
  • The Next.js / Express / Fastify layer. The frameworks node apps use have their own CVE cycles. A node patch does not patch them.

The point is not to dismiss the patch. The point is to make sure the patch is doing what you think it is doing. The team that upgrades node and skips the rest of the security model is the team that files the postmortem six months later.

The case for moving off 20.x before 2027

20.x enters maintenance in October 2026 and end-of-life in April 2027. The release cadence has moved on. As of mid-2026, the active LTS lines are 22.x (Jod) and 24.x (Krypton). The next LTS line (24.x) ships with the same security improvements baked in from the start, plus several years of support.

The case for moving is not “20.x is insecure.” 20.x is fine, and 20.11.1 is the secure version of 20.x. The case for moving is the upgrade cost. A security patch is drop-in. A minor version bump (20 to 22) is not. The longer a team stays on 20.x, the bigger the eventual jump.

A useful exercise: schedule the LTS upgrade before the security patch becomes urgent. Land 20.11.1 this week. Schedule the 22.x migration for the next quarter. Land it on the cadence your team already has, not on the cadence a CVE sets for you.

FAQ

What is Node.js 20.11.1?

A security release for the Node.js 20.x LTS line (codename Iron), published 2024-02-14. It fixes four High-severity CVEs, four Medium-severity CVEs, and disables io_uring by default. It is a drop-in replacement for any 20.x version.

Should I upgrade from 20.10 to 20.11.1?

Yes. The upgrade is semver-patch, meaning no API changes and no breaking changes beyond the security fixes themselves. The cost of the upgrade is one Dockerfile change and one deploy.

What are the most important CVEs in 20.11.1?

The four High-severity CVEs are CVE-2024-21892 (capabilities code injection), CVE-2024-22019 (HTTP chunk extension DoS), CVE-2024-21896 (Buffer path traversal), and CVE-2024-22017 (setuid privilege retention with io_uring). Any of these alone would justify the patch.

Does 20.11.1 disable io_uring?

Yes, by default. Node still supports io_uring behind an opt-in flag. Most apps will not notice. Apps that explicitly enabled io_uring for throughput need to opt back in or accept the throughput regression.

Is 20.x still supported?

20.x is in Active LTS until October 2025 and in Maintenance until April 2026, then end-of-life. Teams running 20.x should plan a migration to 22.x (Active LTS) or 24.x (next LTS) within the next quarter.

How do I check what Node version I am running?

node -p "process.versions.node" returns the exact version string. In a Docker image, run the same command inside the container. In a deployed service, the platform’s runtime inventory is the source of truth.

Does pinning the patch version in my Dockerfile matter?

Yes. A broad pin (node:20-bullseye-slim) silently picks up new patches on every rebuild. A patch pin (node:20.11.1-bullseye-slim) makes the build deterministic and forces an explicit decision for every patch upgrade.

What about 22.x and 24.x?

22.x (Jod) became Active LTS in October 2024. 24.x (Krypton) is the next LTS line, scheduled for late 2025. Both ship with the same security improvements baked in and have longer support windows. The migration cost is the only real reason to stay on 20.x.

What is undici and why does its version matter?

undici is the HTTP client that backs node’s built-in fetch(). Most node apps use it indirectly through fetch, axios, or framework-level wrappers. The 20.11.1 undici bump to 5.28.3 picks up several HTTP-level fixes that affect every app that makes outbound requests.

Does 20.11.1 fix the Node.js permission model?

It tightens it. CVE-2024-21891 and CVE-2024-21890 are both permission-model bypasses. If you use --allow-fs-read and --allow-fs-write in production, the 20.11.1 upgrade closes the path-traversal and wildcard gaps that the previous versions allowed.

Should I run 20.11.1 in production?

Yes. Every team on the 20.x LTS line should be on 20.11.1 or later. The upgrade is drop-in, the fixes are real, and the cost of staying on the previous patch grows with every CVE that ships against it.

How does RunxBuild handle Node version upgrades?

Each RunxBuild service records its runtime version in the service configuration. Bumping the node version is a config change, not a code change, and the platform rebuilds and redeploys automatically. The hosting calculator shows the cost difference between staying on the current LTS and jumping to the next one.

#node 20.11.1#node js#LTS#security#CVE#deployment#npm