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.
| Question | Package vulnerability | Runtime 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 package | Every app running on that runtime build |
| How do I find usage? | Search for imports and calls | Inventory runtimes first, then check triggers |
| Typical trigger | Your code calls a function | Often network input handled before or around your code |
| How is it fixed? | Bump the package, regenerate lockfile | New runtime binary, rebuild artifacts, restart processes |
| What proves the fix? | Lockfile and installed version | The 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) | Severity | Lines | Trigger group | Who is likely in scope |
|---|---|---|---|---|
| Brotli decoder out of bounds write | High | 26, 24, 22 | Called through an API, often indirectly | Anything that decodes Brotli: zlib users, HTTP clients that decode br responses |
| TLS hostname bypass with fullwidth digit IPs | High | 26, 24 | On by default in common work | Services making outbound TLS connections |
| TLS hostname bypass when host loosely resolves to an IP | High | 26, 24, 22 | On by default in common work | Services making outbound TLS connections |
dns.resolve truncates names with a NUL character | High | 26, 24, 22 | Called through an API | Code or libraries using the resolver APIs |
| Keep alive socket processes a response written before the request | Medium | 26, 24, 22 | On by default in common work | HTTP clients using keep alive connections |
| Non canonical IPv4 hostnames let DNS SANs authenticate IP endpoints | Medium | 26, 24, 22 | On by default in common work | Outbound TLS to IP addresses |
| Entropy caches saved into snapshots and single executables | Medium | 26, 24, 22 | Special build | Teams that build startup snapshots or single executable apps |
| ZipFile returns attacker controlled prefix bytes | Low | 26 | Called through an API | Code using that API on 26.x |
| Permission Model over grant via wildcard lookup | Low | 26 | Opt in flag | Apps run with --permission |
Permission Model bypass with fs.realpathSync | Low | 26, 24, 22 | Opt in flag | Apps 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 describes | Can 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 search | Grep node_modules or vendor, check HTTP client decoding defaults |
| A parser, TLS check or protocol handler | Weak; the path is used by default | Assume affected for network facing services |
| Request lifecycle or memory handling (PHP style) | No; the bug can fire before your code runs | Assume affected for anything serving web requests |
| An opt in flag or experimental feature | Not needed | Check start commands, process arguments and config |
| A build time artifact (snapshot, single executable) | Not needed | Check 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 lives | What "fix" means | Easy mistake |
|---|---|---|
| Host OS package or tarball | Upgrade, then restart every process using it | Upgrading without restarting |
| Container base image | Rebuild with fresh pulls, redeploy, roll pods | Reusing a cached layer or never redeploying |
| Single executable or snapshot | Rebuild the artifact with the fixed runtime, redistribute | Upgrading only the build machine |
| Electron app | Upgrade Electron, ship a new app build | Assuming the user's installed Node matters |
| Lambda managed runtime | Check runtime update mode; update functions not on Auto | Leaving Function update or Manual mode functions behind |
| Lambda container function | Rebuild from updated base image, redeploy | Waiting for an automatic update that never comes |
| CI runner or setup step | Bump the pinned version in workflows and tool files | Leaving 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 justification | Plain meaning | Fits a runtime item when |
|---|---|---|
component_not_present | The vulnerable component is not in the product | The image or artifact does not contain that runtime at all |
vulnerable_code_not_present | The component is present, but the vulnerable code is not | Your runtime build excludes the affected module, which is rare for official builds |
vulnerable_code_not_in_execute_path | The code is there, but nothing runs it | An opt in feature is never enabled; a build step is never used |
vulnerable_code_cannot_be_controlled_by_adversary | It runs, but an attacker cannot influence the input | The API only ever processes data you generated yourself |
inline_mitigations_already_exist | Built in protections block the attack | A 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.
- Tie the statement to an artifact, an image digest or build identifier, never
latest. - 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_moduleson this date" is evidence. - Never mark an always on item
not_affectedbecause 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.
| Line | Fixed version (Oct 2026) | Status per project schedule | End of life |
|---|---|---|---|
| Node.js 22 | 22.23.4 | Maintenance LTS | 30 April 2027 |
| Node.js 24 | 24.22.1 | LTS; maintenance phase scheduled from 20 October 2026 | 30 April 2028 |
| Node.js 26 | 26.12.1 | Current; Active LTS scheduled from 28 October 2026 | 30 April 2029 |
| Node.js 20 and older | None | End of life | 20.x ended 30 April 2026 |
| PHP 8.5 | 8.5.12 | Active support | 31 December 2029 |
| PHP 8.4 | 8.4.27 | Active support | 31 December 2028 |
| PHP 8.3 | 8.3.36 | Security fixes only | 31 December 2027 |
| PHP 8.2 | 8.2.35 | Security fixes only | 31 December 2026 |
| PHP 8.1 and older | No project release | End of life | Already 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:
- Upgrade to a supported line now.
- Use a supported backport source, such as a distribution package or extended support build, and record which.
- Build from source with published backports, and accept that you own that build.
- 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.
- 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 situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
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
- Node.js: Wednesday, October 21, 2026 Security Releases
- Node.js: End of life
- Node.js Release schedule (GitHub)
- Node.js: Security policy and threat model (SECURITY.md)
- Node.js: zlib module documentation
- Node.js: process module documentation
- NVD API: CVE-2024-22025
- PHP: Supported versions
- PHP: News archive 2026, including the 21 October security releases
- OpenVEX specification
- CISA: Minimum Requirements for Vulnerability Exploitability eXchange
- CycloneDX: VEX capabilities
- AWS Lambda: Understanding how Lambda manages runtime version updates
- Docker Docs: docker buildx build reference
- Go: Vulnerability management
Last reviewed: 23 October 2026. Release dates and support windows change; check the linked project pages for current status.