Skip to content
← All posts
· 5 min read·Emre Yurtbay

Node.js security updates of June 2026: patch your container and harden it with the Permission Model

Node.js patched twelve CVEs on 18 June 2026. Here is how to update your container to 22.23.0/24.17.0/26.3.1 and harden the app with minimal effort.

Node.jsSecurityCVEDockerContainerPermission ModelDevOpsHardening

On 18 June 2026 the Node.js project shipped security releases across all active lines: twelve CVEs in total, two of them rated HIGH and several affecting the Permission Model. If you run Node.js in a container, this is a good occasion to do two things at once: lift the runtime to the patched version and secure the app with the Permission Model as a lean baseline hardening. This article shows both with a minimal, runnable example.

What was patched

The fixed versions are 22.23.0 (LTS), 24.17.0 and 26.3.1. The affected lines are essentially 22.x, 24.x and 26.x. Two issues are rated HIGH:

  • CVE-2026-48933 (HIGH): an integer overflow in WebCrypto. If the input to subtle.encrypt() is a multiple of 2 GiB, the process can crash (denial of service).
  • CVE-2026-48618 (HIGH): a hostname bypass in TLS verification caused by inconsistent handling of Unicode dot separators. A normalization mismatch between resolver and verifier lets an attacker bypass the wildcard depth of hostname checking.

On top of that come several MEDIUM findings (among them HTTP/2 memory growth, SNI matching issues, a leak of proxy credentials in error messages) as well as three Permission Model bypasses rated LOW, such as CVE-2026-48617 via process.report.writeReport() and CVE-2026-48935 via FileHandle.utimes(). The full list is in the official announcement. The key takeaway: patching is mandatory, and the Permission Model is a sensible but not a load-bearing sole defence.

Step 1: check the version inside the container

First find out what actually runs. It is not the image tag that counts, but the runtime inside the container:

docker run --rm node:24-alpine node --version

If that reports a version below 24.17.0 (or 22.23.0 on the 22 line, or 26.3.1 on the 26 line), the container is vulnerable. For an already running deployment:

docker compose exec app node --version

Step 2: update to the patched version

The simplest route is a precise image tag. Instead of a moving node:24, pin the concrete patch version and rebuild the image:

# Multi-stage: lean, patched runtime image
FROM node:24.17.0-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .

FROM node:24.17.0-alpine
WORKDIR /app
# Do not run as root: the Alpine image ships a node user
COPY --from=build --chown=node:node /app /app
USER node
EXPOSE 3000
CMD ["node", "server.js"]

Then rebuild and re-check the version:

docker compose build --pull app
docker compose up -d app
docker compose exec app node --version   # -> v24.17.0

The --pull flag forces Docker to fetch the base image fresh from the registry instead of reusing a stale local layer. An exact tag like 24.17.0-alpine is reproducible; a loose 24 tag will get the update eventually, but you do not know when.

Step 3: harden the app with the Permission Model

The Permission Model is stable (no longer experimental) since the current LTS line. With the --permission flag Node.js starts in lockdown mode: access to the file system, spawning child processes, worker threads and native addons are all denied initially. You grant only what the app truly needs.

A minimal server that reads files from a data directory looks like this:

// server.js
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';

createServer(async (req, res) => {
  try {
    const data = await readFile('/app/data/index.html');
    res.writeHead(200, { 'content-type': 'text/html' });
    res.end(data);
  } catch {
    res.writeHead(500);
    res.end('error');
  }
}).listen(3000);

You start it with the Permission Model active and exactly one grant: read access to /app/data:

node --permission --allow-fs-read=/app/data server.js

If the app now tries to read outside /app/data or to write anywhere, Node.js throws an ERR_ACCESS_DENIED – write access was never granted. If the app additionally needs write rights or child processes, add further flags deliberately:

node --permission \
  --allow-fs-read=/app/data \
  --allow-fs-write=/app/data/uploads \
  --allow-child-process \
  server.js

Further flags of the same family are --allow-worker (worker threads) and --allow-addons (native addons). In the Dockerfile this goes into the CMD:

CMD ["node", "--permission", "--allow-fs-read=/app", "--allow-fs-write=/app/data", "server.js"]

The flow at a glance:

   +-------------------------+
   |  node --permission ...  |
   +------------+------------+
                |
                v
   +-------------------------+
   |  everything denied      |
   |  (fs, child_process,    |
   |   worker, addons)       |
   +------------+------------+
                |
       only explicit grants
                |
                v
   +-------------------------------------+
   |  --allow-fs-read=/app/data          |
   |  --allow-fs-write=/app/data/uploads |
   +-------------------------------------+

Three common pitfalls

1. The Permission Model does not cover network restrictions on the LTS line. On the 22.x and 24.x lines there is no grant that restricts outbound or inbound network connections – the app may still open sockets without limit. A network grant (--allow-net) exists only from Node.js 25 onwards, and thus on the 26 line. So for network boundaries keep relying on container networks, firewalls or a reverse proxy, not on the Permission Model.

2. Path granularity of --allow-fs-*. Grants apply per path. --allow-fs-read=/app/data allows exactly that tree; access to /app/config fails. A directory is implicitly treated as a tree (the model appends a wildcard internally). Overly broad grants like --allow-fs-read=* effectively remove the protection – grant as narrowly as possible and, if needed, list several paths individually.

3. Transitive dependencies run in the same security context. The Permission Model does not distinguish between your code and that of a library from node_modules. A package that suddenly spawns a child process fails on the missing --allow-child-process just like your own code – that is intended, but it can surprise you. After enabling it, check which grants your dependencies really need instead of pre-emptively opening everything.

Where to go from here

Your container is now up to date and the app baseline-hardened. That is enough to get started; if you want to go deeper, the details are in the official announcement of the June 2026 releases, in the permissions documentation and in the Node.js security overview. Sensible next steps are pinning base images by digest and a regular look at the Node.js security blog, so the next update is not another accidental discovery.

Note: The articles on this blog are produced with the help of AI and are editorially reviewed before publication. Editorial responsibility lies with Emre Yurtbay (see the Impressum).

Discuss your project