Safeguard
Supply Chain Security•

Does a Node.js or PHP Security Release Affect My App?

Usually yes. Runtime flaws sit on paths every request uses. Map each fix to your app, patch on your release line, rebuild images, verify the binary.

Safeguard Security Team
36 min read

Usually yes, even if you never call the feature named in the advisory. A runtime flaw often sits on a path your app uses without asking: request parsing, TLS checks, response decoding inside a library. Treat a Node.js or PHP security release as affecting you by default, prove otherwise item by item, then upgrade on your current release line, rebuild every image and confirm the running binary changed.

That answer holds most of the time, but not every time. Some fixes in a runtime release only matter if you switched on a specific flag or built a specific kind of artifact, and for those a written "not affected" is honest and useful. The work is telling the two apart quickly, and then doing the parts of patching that people skip: rebuilding images that carry their own copy of the runtime, restarting long running workers, and checking that the process serving traffic is actually the fixed one.

This week made the question concrete. On 21 October 2026 Node.js shipped security releases for its 22.x, 24.x and 26.x lines fixing ten CVEs, one of them a Brotli decoder flaw the project says permits remote code execution. The same day PHP shipped 8.5.12, 8.4.27, 8.3.36 and 8.2.35 for two critical flaws in request handling. The most common question in developer forums was some version of "we don't use Brotli" or "we don't run that SAPI, so do we really have to rebuild everything?" This post answers that question in general terms, using those two releases only as examples. For the incident details, see our posts on the Node.js Brotli decoder flaw and on finding and patching every PHP runtime.

Key takeaways

  • Default to affected. Assume a runtime security release applies until you have checked each item against how you deploy.
  • Sort fixes by trigger, not feature name. Some flaws fire on every request or TLS connection, some need an API call, some need an opt in flag. Only the last group is often safe to call "not affected".
  • Indirect use is the usual case. Frameworks and HTTP clients call runtime APIs for you. Node's fetch() decodes Brotli on its own, per NVD.
  • Upgrade on your line first. Take the fixed patch release of the line you run. Plan major moves separately.
  • Rebuild anything that carries its own runtime. Images, single executables, Electron apps and Lambda container functions ignore the host's version.
  • Verify the process, not the package, and write every "not affected" decision down as VEX.
  • End of life lines get nothing. Node.js says end of life versions are "always affected" when a security release occurs.

How is a runtime vulnerability different from a package vulnerability?

When a vulnerability lands in an npm or Composer package, triage starts with a narrow question: is that package in my lockfile, and at which version? A runtime vulnerability is different in four ways.

The runtime is not in your lockfile. Node.js and PHP come from a base image, an OS package, a version manager, a CI setup step or a hosting platform. Tools that only read lockfiles miss the interpreter underneath. The first triage step is inventory, not code search.

Every app on that runtime shares the vulnerable code. The Node.js Brotli decoder ships inside every Node binary. PHP's request startup and shutdown code runs for every request, whether or not anyone on the team has heard of the affected module.

The trust boundary is the runtime's, not yours. The Node.js threat model in the project's SECURITY.md says Node trusts the application code and dependencies it runs, but not data arriving on inbound connections or coming back on outbound ones such as HTTP client and DNS responses. So a runtime vulnerability is usually one where untrusted network data makes the runtime misbehave, on paths an attacker can reach without your code doing anything unusual.

The fix is a new binary that has to reach every copy. You cannot npm update your way to a patched Node. Every image, server, function and desktop app that bundles the runtime has to pick up the new build.

QuestionPackage vulnerabilityRuntime vulnerability
Where is the version recorded?Lockfile (package-lock.json, composer.lock)Base image tag, OS package, .nvmrc, CI setup step, platform setting
Who is affected?Apps that installed the packageEvery app running on that runtime build
How do I find usage?Search for imports and callsInventory runtimes first, then check triggers
Typical triggerYour code calls a functionOften network input handled before or around your code
How is it fixed?Bump the package, regenerate lockfileNew runtime binary, rebuild artifacts, restart processes
What proves the fix?Lockfile and installed versionThe version reported by the running process

Sort every fix by what triggers it

Stop reading feature names and start reading triggers. For each item in a runtime advisory, ask what has to happen for the vulnerable code to run. Most items fall into one of four groups.

Always on. The code runs in normal operation. According to php.net, October's two PHP flaws affect all installations serving web requests, including PHP-FPM, mod_php and php-cgi, with risk highest for mod_php, where an unauthenticated request can trigger the attack before any application code runs. If the bug fires before your code executes, no analysis of your code can show you are safe.

On by default in common work. The code runs whenever you use a very common capability, even through a library. Two of the four High rated Node.js fixes this month are TLS hostname bypasses, and any service making outbound HTTPS calls uses hostname verification on every connection.

Called through a specific API. The code runs only when something calls a particular function, such as the Brotli decompression functions in zlib or dns.resolve. Here "we don't use it" can be true, but only after you account for every library that might call the API for you.

Behind an opt in or a special build. The code runs only with an experimental flag or a particular artifact. Two Low rated Node items concern the opt in Permission Model (--permission), and one Medium item concerns random values cached into startup snapshots and single executable applications. If you use neither, those items do not touch you.

Here is how the October Node.js items map to those groups, using the titles and severities on the Node.js security release page. The grouping is our reading, not something Node.js publishes.

Node.js item (October 2026)SeverityLinesTrigger groupWho is likely in scope
Brotli decoder out of bounds writeHigh26, 24, 22Called through an API, often indirectlyAnything that decodes Brotli: zlib users, HTTP clients that decode br responses
TLS hostname bypass with fullwidth digit IPsHigh26, 24On by default in common workServices making outbound TLS connections
TLS hostname bypass when host loosely resolves to an IPHigh26, 24, 22On by default in common workServices making outbound TLS connections
dns.resolve truncates names with a NUL characterHigh26, 24, 22Called through an APICode or libraries using the resolver APIs
Keep alive socket processes a response written before the requestMedium26, 24, 22On by default in common workHTTP clients using keep alive connections
Non canonical IPv4 hostnames let DNS SANs authenticate IP endpointsMedium26, 24, 22On by default in common workOutbound TLS to IP addresses
Entropy caches saved into snapshots and single executablesMedium26, 24, 22Special buildTeams that build startup snapshots or single executable apps
ZipFile returns attacker controlled prefix bytesLow26Called through an APICode using that API on 26.x
Permission Model over grant via wildcard lookupLow26Opt in flagApps run with --permission
Permission Model bypass with fs.realpathSyncLow26, 24, 22Opt in flagApps run with --permission

Read in aggregate, the table says a team that never touches Brotli, DNS or the Permission Model still makes outbound TLS connections, and three of the ten items sit on that path. For nearly every Node.js service, the release applies even if most items do not.

The release also updated c-ares, OpenSSL (22 and 24), SQLite (22), Undici and the bundled CA certificates, which Node.js labels an eMudhra intermediate CA compromise change. That is another reason to take the whole release rather than cherry pick. Our post on trust store updates across Python, Node, Go and Electron covers the certificate side.

Can reachability analysis tell you whether your code calls the vulnerable runtime API?

Partly. It is a useful signal for the "called through an API" group and close to useless for the "always on" group.

Reachability analysis builds a call graph from your entry points and asks whether any path leads to a known vulnerable function. The Go project describes govulncheck as surfacing only vulnerabilities that affect you, based on which functions your code transitively calls. Our post on whether govulncheck reports standard library vulnerabilities covers that case. For interpreted runtimes the picture is messier, for reasons that apply to any tool.

The vulnerable function may not be one you can name. Node's advisory names a component, the Brotli decoder, not a function. A tool needs a mapping from the advisory to the public APIs that lead into it. For Brotli, Node's zlib documentation lists them: zlib.brotliDecompress(), zlib.brotliDecompressSync(), zlib.createBrotliDecompress(), the BrotliDecompress class and newer iterable variants. For a TLS hostname check, the entry point is nearly the whole network stack.

Dynamic code defeats static graphs. Computed property access, require() with a variable, PHP variable functions and dependency injection containers all hide calls from a static analyzer.

Some paths start below your code. The PHP flaws can fire before application code runs. Node's HTTP parser, TLS layer and DNS client work on behalf of your code. There is no call from your code to find.

Native addons and subprocesses are invisible to call graph analysis over your JavaScript.

So use reachability where it is strong. If an advisory names a specific public API, a result showing no path from your code and dependencies to it is real evidence, especially alongside a text search of node_modules or vendor. If it names request handling, TLS validation, protocol parsing or memory management, treat it as reachable for any network facing service.

Advisory describesCan reachability help?What to rely on instead
A named public API (for example a decompression function)Yes, as strong evidence when combined with a dependency searchGrep node_modules or vendor, check HTTP client decoding defaults
A parser, TLS check or protocol handlerWeak; the path is used by defaultAssume affected for network facing services
Request lifecycle or memory handling (PHP style)No; the bug can fire before your code runsAssume affected for anything serving web requests
An opt in flag or experimental featureNot neededCheck start commands, process arguments and config
A build time artifact (snapshot, single executable)Not neededCheck your build pipeline for those steps

What about indirect use: a dependency, a proxy, or the framework calling the feature for you?

This is where "we don't use it" most often turns out to be wrong.

Your HTTP client decodes for you. Node's built in http request APIs hand you the raw body; the zlib documentation's client example pipes br responses through createBrotliDecompress() by hand. Many HTTP client libraries do that for you by default. Node's global fetch() does it always: NVD's record for CVE-2024-22025, an earlier Node.js denial of service, says fetch() always decodes Brotli content. If your service calls an API with fetch(), your process decodes Brotli whenever that server, or anything in front of it, sends a Brotli body.

Your framework inflates request bodies. Body parsing middleware can inflate compressed request bodies before your handler runs. Whether it handles Brotli by default depends on the library and version, so check the docs for the version in your lockfile.

A dependency decompresses on a path you forgot. Archive extractors, asset tools, crawlers, storage SDKs and build tools all decompress data. Build time use still matters if your build processes untrusted input such as pull requests from forks.

A fast way to check is to search the installed tree, not just your source:

# Direct Brotli decompression calls in your code and installed dependencies
grep -rnE "brotliDecompress|createBrotliDecompress|BrotliDecompress|decompressBrotli" \
  --include='*.js' --include='*.mjs' --include='*.cjs' --include='*.ts' \
  src/ node_modules/ 2>/dev/null | grep -v "\.d\.ts" | head -50

# Places that use fetch() or decode content-encoding headers
grep -rnE "content-encoding|\bfetch\(" --include='*.js' --include='*.mjs' --include='*.ts' src/ | head -50

# Direct use of the resolver APIs named in the DNS item
grep -rnE "dns\.resolve|dns\.promises\.resolve|resolver\.resolve" src/ node_modules/ 2>/dev/null | head -50

A hit in node_modules does not prove the path runs in production, and no hit does not prove it never does. But a hit in a library your service calls on every request ends the argument.

Proxies are a separate question. A reverse proxy or gateway that decompresses traffic uses its own decoder, may need its own patch, and does not protect Node from content it passes through unchanged. Do not count it as a mitigation unless it rejects the specific input the advisory describes. For PHP this month, the project says there are no complete mitigations and updating is the only safe choice.

Why do advisories sometimes say "rebuild", not "upgrade"?

Because "upgrade" assumes the runtime lives in one place. When it is copied into a build artifact, upgrading the host changes nothing about the artifact. You build it again from the fixed runtime and redeploy. Here is where runtimes hide.

Container images

Your image inherits the runtime in its base image at build time. A new upstream tag does nothing until you rebuild and redeploy, and the build cache may keep the old layers: Docker's --pull flag makes the build "always attempt to pull all referenced images", and --no-cache stops layer reuse. Upgrading packages inside a running container is not a fix either. See whether to apt upgrade inside a container or rebuild the image.

Single executables, snapshots and Electron apps

Node single executable applications and startup snapshots bake a copy of the runtime, and in October's entropy item some runtime state, into the artifact. Electron bundles its own Node.js and ships Node fixes on its own schedule, so an Electron app runs your copy of Node, not the user's. In all three cases you rebuild with the fixed version and redistribute.

Serverless and hosting platforms

AWS documents three Lambda runtime update modes. Auto, the default, applies new runtime versions automatically. Function update applies them only when you update the function, so an untouched function stays behind. Manual keeps the pinned version indefinitely. Container image functions must be rebuilt from the updated base image and redeployed. Many hosting platforms likewise apply patched runtimes on the next build or deploy, not to running instances.

PHP behind long running workers

PHP-FPM pools and Apache with mod_php keep workers alive across many requests. A package upgrade replaces files on disk, but existing workers run the old code until restarted, which is why PHP's October guidance keeps repeating "restart every worker".

Where the runtime livesWhat "fix" meansEasy mistake
Host OS package or tarballUpgrade, then restart every process using itUpgrading without restarting
Container base imageRebuild with fresh pulls, redeploy, roll podsReusing a cached layer or never redeploying
Single executable or snapshotRebuild the artifact with the fixed runtime, redistributeUpgrading only the build machine
Electron appUpgrade Electron, ship a new app buildAssuming the user's installed Node matters
Lambda managed runtimeCheck runtime update mode; update functions not on AutoLeaving Function update or Manual mode functions behind
Lambda container functionRebuild from updated base image, redeployWaiting for an automatic update that never comes
CI runner or setup stepBump the pinned version in workflows and tool filesLeaving a cached toolcache version in use

If not reachable, is "do not patch" ever acceptable?

Sometimes, for a specific item, for a specific product, for a defined time. Rarely for a whole runtime release.

There are real cases. A private batch job with no outbound TLS and no compressed input is not exposed to most of this month's Node items. A team never enabling the Permission Model can let those Low items wait for the next rebuild.

What makes the decision acceptable is the record: which vulnerability, which artifact, what status, why, what evidence, who decided and when to review. The format for that record is VEX, the Vulnerability Exploitability eXchange. CISA published minimum elements for VEX in April 2023, and OpenVEX and CycloneDX's vulnerability analysis fields are the two common machine readable formats.

OpenVEX defines four statuses: not_affected, affected, fixed and under_investigation. A not_affected statement must carry either a justification or an impact statement, and the spec lists five justification labels:

OpenVEX justificationPlain meaningFits a runtime item when
component_not_presentThe vulnerable component is not in the productThe image or artifact does not contain that runtime at all
vulnerable_code_not_presentThe component is present, but the vulnerable code is notYour runtime build excludes the affected module, which is rare for official builds
vulnerable_code_not_in_execute_pathThe code is there, but nothing runs itAn opt in feature is never enabled; a build step is never used
vulnerable_code_cannot_be_controlled_by_adversaryIt runs, but an attacker cannot influence the inputThe API only ever processes data you generated yourself
inline_mitigations_already_existBuilt in protections block the attackA specific, verified control prevents the triggering input

Here is an OpenVEX document recording that an image is not affected by the Permission Model wildcard item because the service never runs with --permission:

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://example.com/vex/orders-api-2026-10-23",
  "author": "Platform Security <security@example.com>",
  "timestamp": "2026-10-23T09:00:00Z",
  "version": 1,
  "statements": [
    {
      "vulnerability": { "name": "CVE-2026-106071" },
      "products": [
        {
          "@id": "pkg:oci/orders-api@sha256%3A4f1c0000000000000000000000000000000000000000000000000000000000aa",
          "subcomponents": [ { "@id": "pkg:generic/node@26.12.0" } ]
        }
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path",
      "impact_statement": "The service starts with 'node server.js' and never enables --permission. Verified in the Dockerfile CMD and Kubernetes manifest on 2026-10-23. Review when the image is next rebuilt."
    }
  ]
}

Three rules keep VEX honest for runtime items.

  1. Tie the statement to an artifact, an image digest or build identifier, never latest.
  2. Put the evidence in the impact statement. "Not reachable" is an opinion. "Start command verified in the manifest; no Brotli calls in source or node_modules on this date" is evidence.
  3. Never mark an always on item not_affected because your own code does not call it.

VEX is not a substitute for patching once patching is cheap. Write it for the items you can honestly rule out, so the urgent queue holds only what matters, then take the whole release in the next rebuild.

Which release line should you move to when the fix lands on three lines at once?

Move to the fixed patch release on the line you already run. Do that now. Treat a move to a newer major line as a separate project with its own testing.

The October fixes shipped as Node.js 22.23.4, 24.22.1 and 26.12.1, and PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, each a patch release meant as a drop in replacement. Jumping from 22 to 26 mid response mixes a low risk patch with a high risk upgrade; if something breaks, you may roll back the security fix with it. Still, check where each line stands, because the support window decides how many more fixes you get.

LineFixed version (Oct 2026)Status per project scheduleEnd of life
Node.js 2222.23.4Maintenance LTS30 April 2027
Node.js 2424.22.1LTS; maintenance phase scheduled from 20 October 202630 April 2028
Node.js 2626.12.1Current; Active LTS scheduled from 28 October 202630 April 2029
Node.js 20 and olderNoneEnd of life20.x ended 30 April 2026
PHP 8.58.5.12Active support31 December 2029
PHP 8.48.4.27Active support31 December 2028
PHP 8.38.3.36Security fixes only31 December 2027
PHP 8.28.2.35Security fixes only31 December 2026
PHP 8.1 and olderNo project releaseEnd of lifeAlready ended

Node.js dates come from the project's release schedule repository, which says they may change; PHP dates from php.net. On Node 22, take 22.23.4 now and move before April 2027; Maintenance LTS lines keep receiving security updates until then. On PHP 8.2, take 8.2.35 now and plan the move this quarter, since security support ends on 31 December 2026. For more, see which version to pick when an advisory is fixed in several release lines.

What about end of life lines that receive no fix?

Assume they are affected and that no fix is coming from the project.

Node.js says end of life versions are always affected when a security release occurs and receive no updates. Its end of life page notes that commercial providers supported through the OpenJS Foundation's Ecosystem Sustainability Program sell patches for old lines, as a temporary measure while you upgrade. See our post on Node.js 20 end of life.

php.net says PHP 8.1 and older get no project updates, though minimal backports for 7.4, 8.0 and 8.1 were published this month for source builders. Linux distributions often backport fixes into an old version string, so php -v can look old while the package is fixed. Check the distribution's tracker, not the upstream number.

For an end of life runtime you cannot move yet, in rough order of preference:

  1. Upgrade to a supported line now.
  2. Use a supported backport source, such as a distribution package or extended support build, and record which.
  3. Build from source with published backports, and accept that you own that build.
  4. Reduce exposure for a fixed period with request limits, network isolation or the project's partial workarounds, which PHP says reduce exposure but do not make an install safe.
  5. Record a time boxed exception with an owner and a date.

How do you verify the running binary is the patched one?

By asking the process that serves traffic, not the package manager, not the Dockerfile and not the dashboard of a deploy tool. Here are checks that work for each place a runtime lives.

Node.js on a host

# Version of the node on PATH
node -p "process.versions.node"

# Versions of bundled components, including brotli, openssl and undici
node -p "JSON.stringify(process.versions, null, 2)"

# Every node binary on the machine, not just the one on PATH
which -a node
sudo find / -xdev -type f -name node -perm -u+x 2>/dev/null

# Running node processes and when they started
ps -o pid,lstart,args -C node

The Node.js process documentation shows process.versions reporting the Node version alongside bundled components such as brotli, openssl, undici, llhttp, ares and zlib. Compare process start times with the time you installed the upgrade. Any process that started earlier is still running the old code.

On Linux there is a second check. When a package upgrade replaces a binary that a process is still running, the kernel's link for that process points at a file marked as deleted:

# A "(deleted)" suffix means the process is still running a binary that has since been replaced
for pid in $(pgrep -x node); do echo "$pid $(readlink /proc/$pid/exe)"; done

PHP on a host

php -v
php-fpm8.3 -v        # the binary name varies by distribution and version
apachectl -M 2>/dev/null | grep -i php

# Debian and Ubuntu, RHEL family: installed package versions
dpkg -l | grep -E '^ii\s+php'
rpm -qa | grep -i php

# Worker start times, then restart
ps -o pid,lstart,args -C php-fpm8.3
sudo systemctl restart php8.3-fpm

The CLI binary and the FPM binary are separate files and may come from separate packages. Check the one that serves requests. If you use a distribution package, compare against the distribution's advisory, since it may backport the fix without changing the upstream version number.

Container images and Kubernetes

# What the image contains, without trusting the tag
docker run --rm --entrypoint node myorg/orders-api:2026-10-23 -p "process.versions.node"
docker image inspect --format '{{.Id}} {{.Created}}' myorg/orders-api:2026-10-23

# What the running pods report
kubectl exec deploy/orders-api -- node -p "process.versions.node"
kubectl get pods -l app=orders-api -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[0].imageID}{"\n"}{end}'

The imageID field reports the digest the pod actually runs. If it does not match the digest of your rebuilt image, the rollout did not happen.

AWS Lambda

# Node functions and their runtime identifiers
aws lambda list-functions \
  --query "Functions[?starts_with(Runtime, 'nodejs')].[FunctionName,Runtime]" --output table

# How each function receives runtime updates
aws lambda get-runtime-management-config --function-name orders-handler

AWS documents that the runtime version ARN appears in the console and in the INIT_START line of the function's logs. Runtime version numbers in Lambda are not Node.js version numbers, so log process.versions.node at startup if you want a direct answer.

Inside a running app

Log process.versions.node at startup or expose it on an internal endpoint for a fleet view. Never expose versions publicly, and keep PHP's expose_php off on public sites.

A step by step response to a runtime security release

Here is the full sequence, from the moment the advisory lands to the record you keep. It assumes a team with services in containers, some Lambda functions and a few hosts, which covers most of the cases above.

Step 1: Read the advisory and classify each item

Open the project's own advisory, not a summary. For Node.js that is the security release post on nodejs.org; for PHP, the announcement on php.net and the linked GitHub security advisories. For each item, write one line: trigger group (always on, default path, API, opt in or build), lines affected, fixed versions.

Step 2: Inventory every copy of the runtime

# Images you build: which base image and tag each Dockerfile uses
grep -rnE "^FROM\s+(node|php)" --include='Dockerfile*' .

# Pinned versions in tool and CI files
cat .nvmrc .node-version 2>/dev/null
grep -rnE "node-version|php-version" .github/workflows/ 2>/dev/null

# Engines field, which may block or allow the fixed version
node -p "require('./package.json').engines"

Combine this with an image scan or SBOM for each built image, since a base image can be several layers removed from the Dockerfile you are reading.

Step 3: Check opt in features and special builds

# Permission Model, snapshots and single executable apps
grep -rnE "\-\-permission|\-\-build-snapshot|\-\-experimental-sea-config|postject" \
  package.json Dockerfile* .github/ scripts/ k8s/ 2>/dev/null

# PHP settings named in the October workarounds
php -i | grep -E "^(memory_limit|post_max_size|session.upload_progress.enabled|fatal_error_backtraces)"

Items whose trigger you have ruled out go into a VEX draft with the evidence. Everything else goes into the patch plan.

Step 4: Pin the fixed version everywhere it is declared

# Dockerfile: pin the patch release, and ideally the digest
FROM node:24.22.1-bookworm-slim
# Version manager files
echo "24.22.1" > .nvmrc
# GitHub Actions
- uses: actions/setup-node@v4
  with:
    node-version-file: .nvmrc
    check-latest: true

For PHP on hosts, upgrade through your package manager or the source you normally use, then restart. For PHP images, bump the base image tag the same way.

Step 5: Rebuild with fresh layers and redeploy

docker build --pull --no-cache -t myorg/orders-api:2026-10-23 .
docker push myorg/orders-api:2026-10-23
kubectl set image deploy/orders-api app=myorg/orders-api:2026-10-23
kubectl rollout status deploy/orders-api

# Lambda container functions
aws lambda update-function-code --function-name orders-handler \
  --image-uri 123456789012.dkr.ecr.us-east-1.amazonaws.com/orders-handler:2026-10-23

For Lambda functions using a managed runtime in Function update mode, any function update picks up the latest runtime version, so a configuration change or redeploy is enough. Functions in Manual mode need their pinned runtime version changed.

Step 6: Restart everything that keeps the old binary in memory

sudo systemctl restart php8.3-fpm
sudo systemctl restart apache2        # for mod_php
pm2 reload all                        # if you run Node under a process manager

Step 7: Verify from the running process

Run the checks in the previous section against each environment. Record the version reported by the process, the image digest and the time.

Step 8: Keep the record

Store the advisory link, the classification from step 1, the VEX statements from step 3, the versions and digests from step 7 and the date. If a customer, auditor or your own team asks next month whether you were exposed and when you were fixed, this is the answer. For what evidence tends to satisfy auditors, see how to prove a security patch actually fixed the vulnerability.

Common mistakes

Reading the feature name instead of the trigger. "We don't use Brotli" is a statement about your source code. The question is whether your process ever decodes Brotli data, including through a client library or fetch().

Treating a runtime release as a single CVE. A runtime release usually fixes several issues at once and carries dependency updates. You might rule out the headline item and still be affected by a TLS fix in the same release.

Upgrading the host and forgetting the images. The host's Node or PHP has nothing to do with the copy inside a container image, a single executable app or an Electron app.

Rebuilding from a cached layer. A rebuild that reuses the old base layer produces an image with a new tag and the old runtime. Use --pull, and check the version inside the built image before you ship it.

Upgrading packages without restarting workers. PHP-FPM, mod_php, process managers and long running Node services keep the old code in memory. Package version and running version can disagree for days.

Jumping major versions under pressure. Patch on your line first; plan the major move separately.

Assuming a managed platform patched everything. Lambda functions in Function update or Manual mode, and container image functions, do not update on their own.

Trusting the upstream version number on a distribution package. Check the distribution's own advisory; it may backport into an older version string.

Writing "not affected" with no artifact and no date. A VEX statement without a digest, evidence and review date cannot be checked and goes stale silently.

FAQ

Do I need to update Node.js if I don't use Brotli?

Probably yes. The October 2026 Node.js release fixed ten issues, including TLS hostname validation flaws that affect any service making outbound HTTPS connections. Even if no code in your process ever decodes Brotli, the release is likely to apply to you through another item.

Does fetch() in Node.js decompress Brotli automatically?

Yes. NVD's description of CVE-2024-22025 says fetch() always decodes Brotli content, which is why that earlier denial of service affected callers of fetch(). If your service uses fetch() against servers you do not fully control, treat decoder flaws as reachable.

Does a PHP vulnerability affect me if I use PHP-FPM instead of mod_php?

For the October 2026 flaws, yes. php.net lists PHP-FPM, mod_php, php-cgi and other server setups as affected, and says risk is highest for mod_php. Check each advisory's list of affected SAPIs rather than assuming your setup is excluded.

Do PHP CLI scripts need the security update?

php.net scoped the October flaws to installations serving web requests. A CLI only cron job is outside that description, but the CLI and server packages usually update together. Patch them together so you do not have to track two versions.

Is upgrading Node.js on the server enough if my app runs in Docker?

No. The container image carries its own Node binary. Rebuild the image from a fixed base image, pulling fresh layers, then redeploy and confirm the running pods report the new version.

Does AWS Lambda patch Node.js automatically?

For managed runtimes in Auto mode, AWS says Lambda applies runtime updates automatically in a two phase rollout. Functions in Function update mode update only when you change them, Manual mode functions stay pinned, and container image functions need a rebuild and redeploy.

What is VEX and when should I use it for a runtime vulnerability?

VEX is a machine readable statement of whether a product is affected by a vulnerability. Use it when you can show that an item's trigger never occurs in a specific artifact, such as an opt in flag you never set, and include the evidence and a review date.

Which Node.js version should I upgrade to after a security release?

The fixed patch release on the line you already run: 22.23.4, 24.22.1 or 26.12.1 for the October 2026 release. Plan any move to a newer major line as separate work with its own testing.

Is it safe to keep running Node.js 20 or PHP 8.1?

Not without a backport source. Node.js says end of life lines are always affected when a security release occurs and receive no fixes. PHP 8.1 and older get no project updates, though php.net published minimal source backports this month. Upgrade, or use a supported backport and record an exception.

How do I check which Node.js version a running process is using?

Ask the process. Run node -p "process.versions.node" in the same environment, use kubectl exec for pods, and compare process start times with your upgrade time. On Linux, a (deleted) target in /proc/PID/exe means the process still runs a replaced binary.

Fix it without waiting for upstream

Runtime releases are a reminder that the fix has to travel: from the project's release, into your base image or version pin, through a rebuild, and into a restarted process. For the interpreter itself, that path runs through the vendor or distribution. For the open source packages and images you build on top of it, the gaps below are yours to close.

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 covers eight categories, including dependencies, licenses, security posture and findings. 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. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.

3. Gold Registry: start from components that are already fixed

registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature 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. This fork and patch flow is in early access today, so 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 shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
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

On a runtime release, Safeguard helps with inventory, priority and the change itself. Image scans (safeguard scan --image orders-api:latest) inventory what is in the image layers, the platform shows which projects contain a given component and version, reachability ranks "affected but not reachable" findings lower honestly, and VEX can record the reasoning. Safeguard does not patch the interpreter on hosts you manage, restart your PHP workers or ship your Electron app, and the runtime fix itself still comes from the Node.js or PHP project or your distribution.

This demo is a real CLI session. You ask the assistant to fix the lodash CVE in a repository; it reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, and pushes nothing. The same approval flow works for a runtime release: ask it to move a Dockerfile's base image, .nvmrc and workflow pin to the fixed patch release, then approve the build and test commands one at a time.

Click through it at your own pace, or open it full screen.

This demo sets up the Safeguard runner on your own machine, approves it with its device code and scans from the terminal. It covers step 2 of the walkthrough: running scans of your repositories and images on hardware you control, so that when the next runtime release lands, the list of images that carry the old version already exists.

Click through it at your own pace, or open it full screen.

This demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude can use. During a release like this one, an AppSec engineer can ask which projects carry a given component and which findings rank highest, while deciding exactly which tools the assistant is allowed to call.

Click through it at your own pace, or open it full screen.

You can download Safeguard at safeguard.sh/download and read the CLI documentation at docs.safeguard.sh/docs/cli.

Quick check

Your Node.js service never calls zlib, but it calls a partner API with fetch(). Is a Brotli decoder flaw reachable?

Treat it as reachable. NVD's record for an earlier Node.js CVE says fetch() always decodes Brotli content, so a Brotli encoded response from that API, or from anything that can sit in front of it, goes through the decoder without your code asking.

You upgraded the node package on a host, but your app runs in a container on that host. Are you patched?

No. The container image carries its own copy of Node. Rebuild the image from a fixed base image with fresh layers, redeploy, and check the version reported inside the running container.

Sources

Last reviewed: 23 October 2026. Release dates and support windows change; check the linked project pages for current status.

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.