After being in crisis mode at work with PHP2Shell, a new vulnerability on NodeJS surfaced.. 😅
I wanted to take a closer look at Node2Shell and what it means for the nodejs applications running in our infrastructure.
The first thing I wanted to understand was whether putting NGINX, Apache, an API gateway or a WAF in front of Node.js changes the risk. Many production applications already have several of these layers. We don't normally expose the application directly without a reverse proxy. We need to know what they do with the compressed data before we can decide whether they help.
There is also an outbound path to consider. A service can encounter this vulnerability while fetching data from another server, even when nobody can connect to it directly from the internet.
For me, that makes this an application and infrastructure patching exercise. We need to find the vulnerable decoder, understand what can reach it, and verify that the replacement is running.
What is Node2Shell?

Node2Shell is the name used for CVE-2026-111497, a memory corruption vulnerability in Google's Brotli decoder, which Node.js includes for decompression.
Brotli makes data smaller for storage or transmission. On the web, it commonly appears as br in HTTP content encoding. The receiving application decodes the compressed bytes to recover the original content.
The vulnerable decoder can mishandle a specially crafted stream and write outside its intended memory buffer. The possible consequences include a process crash, disclosure of process memory and remote code execution. Google's CVE record assigns a CVSS v4.0 score of 9.2, Critical; the Node.js project rates its own exposure as High. CVE record, Node.js advisory.
The name can make this sound specific to Node.js. The underlying component is shared with other software, which is why coverage also uses names such as HTTP2Shell and Broccoli2Shell. It can be reached outside HTTP too, so restricting HTTP/2 would not resolve it. JFrog's broader analysis.
What happened?
Node.js released security updates on 21 October 2026. Its advisory credits OpenAI Codex Security researchers Khai Tran and Sylvie Mayer with reporting the vulnerability. Official disclosure.
The evidence then developed beyond an initial crash demonstration:
- On 22 October, Imperva demonstrated code execution on the official Node.js 24.22.0 Linux arm64 image. It reproduced the problem through incoming request bodies and fetched responses. The attack required multiple attempts; Node.js 24.22.1 rejected the same inputs. Imperva's original research.
- Python package advisories documented that
Brotliandbrotlicffialso ship the vulnerable decoder. PyPA Brotli advisory, brotlicffi advisory. - ELLIO subsequently reproduced code execution on an official x86-64 Node.js build in several application setups. Its 26 October update reports exploit traffic observed in the wild. That is a report of observed attack traffic, rather than evidence that every vulnerable deployment has been compromised. ELLIO's research and update.
The distinction matters when reading older articles. A statement that no exploitation had been reported on 22 October should not be carried forward as the current position on 27 October.
How does the vulnerability work?
A decoder has to distinguish between incomplete input and invalid input. If a stream is incomplete, waiting for the next chunk can be correct. If the stream describes something impossible, the decoder needs to stop.
In this case, an invalid Brotli block switch was treated as a temporary shortage of input. The streaming decoder continued accepting bytes into an eight-byte internal buffer and could write beyond it. The problem occurs in native code underneath the JavaScript API. Node.js technical description.
The upstream patch gives the invalid switch a dedicated format error, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH, and propagates that failure back through the decoder. It can then reject malformed data instead of continuing as though another chunk will make it valid. Google's patch.
Memory corruption does not guarantee a shell on every machine. Turning it into code execution depends on the runtime build, architecture and memory layout. Imperva's successful demonstration establishes that RCE is practical in its tested environment; it does not establish an identical exploit for every platform or application. Demonstration scope.
Even an attempt that only crashes the process can interrupt a service. We should not wait for an attacker to achieve reliable code execution before treating the affected path seriously.
Where can the malicious data enter?
I would check three paths: incoming request bodies, responses fetched by the application, and direct decompression of files or other data.

Incoming request bodies
A client can send a request with a Brotli-compressed body:
Content-Type: application/json
Content-Encoding: br
Express 5 and body-parser 2 can inflate Brotli bodies through their JSON, raw, text and URL-encoded parsers. A parser runs when the request matches its configured media type, and its inflate option defaults to true. Express body-parser documentation.
The relevant path can therefore look like this:
Client sends compressed body
→ Reverse proxy or gateway forwards it
→ Application body parser decompresses it
→ Vulnerable Brotli decoder processes attacker-controlled bytes
A bare Node.js HTTP server does not automatically decompress an incoming body merely because it carries Content-Encoding: br. Middleware or application code has to pass the bytes to a decoder. Akamai's explanation of the entry points.
Middleware order also matters. If a global body parser runs before authentication, decompression can happen before the application checks the user's credentials. A login requirement only protects this path when the request is rejected before it reaches the decoder.
Responses fetched by the application
Node.js's built-in fetch() automatically decompresses Brotli-encoded responses. A malicious or compromised remote server can therefore supply the problematic data to a vulnerable client application. Akamai's analysis.
Consider a service that fetches URLs for link previews. The inbound request might be ordinary JSON containing a URL. The compressed payload arrives later, in the response from the server at that URL.
Application fetches an external URL
→ Remote server returns a Brotli-encoded response
→ Application automatically decompresses the response
→ Vulnerable decoder processes the remote server's bytes
I would include crawlers, document downloaders, integration workers and URL-fetching agent tools in that review. The same reasoning applies to a scheduled job without a public listening port.
Explicit decompression and non-HTTP data
Applications can also call APIs such as brotliDecompress() or createBrotliDecompress() directly on untrusted input. Akamai's entry-point analysis.
The wider inventory should include software processing Brotli inside other formats. JFrog examined affected paths in media and data-processing software, including JPEG XL, Arrow/Parquet and ClickHouse. Its results varied between memory corruption, hangs and crashes; they should not all be described as demonstrated RCE. JFrog research.
Does NGINX or Apache in front protect the application?
It depends on what that layer does with the request.
NGINX normally forwards request bodies and request headers to an upstream. From that documented behaviour, a forwarding configuration can leave the compressed body available for the application to decode. Terminating TLS or buffering the request does not establish that the Brotli stream has been validated. NGINX proxy module documentation.
The same architectural question applies to Apache acting as a reverse proxy: does it reject the encoded request, transform it, or forward it for the backend to process? Apache proxy documentation.
Apache's standard mod_brotli is an output compression filter. Its presence is not evidence that Apache is decoding incoming Brotli bodies, and turning off that output filter does not disable decompression in Node.js behind it. Apache mod_brotli documentation.
Two headers are easy to mix up:
| Header | What it describes |
|---|---|
Content-Encoding: br | This message's body is Brotli encoded. It can appear on a request or a response. |
Accept-Encoding: br | The client says it can accept a Brotli-encoded response. |
Removing Accept-Encoding at the public reverse proxy changes response negotiation on that connection. It does not prevent a client from sending a compressed request body, or stop the backend's separate outbound HTTP client from decoding responses.
An explicit rejection policy before forwarding can close an incoming request path. The policy still needs to cover every relevant route, and the origin must not be reachable through another endpoint that bypasses it.
What about API gateways on AWS, Azure, GCP and Alibaba Cloud?
I would avoid giving a single answer for all cloud gateways. Products and integration modes handle bodies differently.

These are conclusions about delivery paths, rather than certifications that a backend is patched. A gateway might reject a Brotli-encoded HTTP body while allowing an uploaded file that application code later decompresses. It also does not ordinarily sit in the path of a separate response fetched by the backend.
If the gateway itself uses a Brotli decoder, its patch status is another item to establish. For a managed service, request the provider's applicable security guidance. For a self-managed gateway, check the product build and bundled libraries.
Can a WAF block Node2Shell?
A WAF can help block an incoming attack when an applicable rule rejects it before the vulnerable decoder runs. That could be a policy rejecting unsupported content encodings or a vendor rule specifically addressing this vulnerability.
However, a WAF being enabled tells us very little about whether it understands a malformed compressed stream.
Microsoft documents that Azure Front Door WAF does not support content encoding and does not decompress compressed bodies before sending them to the WAF engine. Google Cloud Armor also documents decoding limitations, with unsupported encodings inspected as raw data. Azure Front Door WAF FAQ, Cloud Armor documentation.
Inspection limits matter too. AWS WAF inspects a bounded portion of the body, depending on the protected service and configuration. An allowed request can still be forwarded in full, including bytes outside the inspected portion. AWS WAF inspection limits.
For a temporary WAF control, I would check:
- The rule is applied to the affected hostname and route, in blocking mode.
- Rule ordering and exclusions do not allow the request through first.
- Header normalization, repeated headers and encoding lists cannot bypass the policy.
- Oversized and uninspectable requests have deliberate handling.
- Direct access to the origin cannot bypass the WAF.
Imperva reports adding WAF protections and testing them against its reproduced attacks. That is evidence for its tested protection, rather than a promise about every WAF deployment. Imperva's mitigation discussion.
The application fetching a malicious compressed response remains a separate concern. An inbound WAF rule does not cover that path.
Why does this matter beyond a crashed application?
Successful code execution starts with the permissions of the affected process. ELLIO's demonstrations included shells running as the service account, including root where the container's service ran as root. ELLIO's lab results.
The practical impact then depends on what that process can access: database credentials, application secrets, mounted files, cloud identities and internal services. Running inside a container does not remove those permissions.
For an infrastructure team, I would pair the patch review with a permissions review. Does this workload need its current service account? Can it reach unrelated applications? Are administrative credentials or a container-runtime socket mounted into it?
Those controls can reduce the consequences of a compromise. They do not correct the decoder.
How to patch Node2Shell
Update the runtime that the application uses
The first upstream Node.js releases containing the fix are:

Use a supported release containing the fix. If an application still depends on an end-of-life line such as Node.js 18 or 20, plan its move to a supported line; an upstream security patch should not be assumed to exist for that old branch. Node.js release and support status.
Updating Express or running npm audit fix does not replace the Node.js executable bundled into an application image. Likewise, upgrading Node.js on a Docker host does not replace Node.js inside its containers.
For distribution-provided builds, check the vendor's security advisory and package revision. A vendor may backport the fix, and some builds use a system Brotli library. Raw upstream version comparisons are not enough to establish their status. Debian's CVE tracking.
Check the other copies of Brotli
Affected unpatched upstream Brotli releases include 1.0.3 through 1.0.9, 1.1.0 and 1.2.0. Google released fixes in 1.0.10, 1.1.1 and 1.2.1. This is why “everything below 1.2.1 is vulnerable” is too broad: fixed older branches and vendor backports exist. CVE details, Google Brotli releases.
For Python environments, the relevant fixed package versions are Brotli 1.2.1 and brotlicffi 1.2.1.0. HTTP libraries can use these bindings automatically when installed, so an application does not have to import brotli explicitly to reach the decoder. PyPA Brotli advisory, brotlicffi advisory.
Check each application's environment:
python -m pip show Brotli brotlicffi
Update whichever affected packages are already installed through the project's dependency manager, regenerate its lock file and rebuild the deployment. There is no need to install a Brotli binding into an application that does not use one.
Other bundled copies need their own product updates. For example, Brotli4j's advisory explicitly warns that patching the operating system's library does not replace the native copy inside its JARs. Microsoft's .NET advisory similarly requires rebuilding self-contained applications and republishing NativeAOT applications with patched components. Brotli4j advisory, .NET advisory.
What infrastructure engineers need to do
I would start with the deployed workloads and work back to how they were built.
For each service, record its owner, running runtime or vendor package, image digest, exposure paths and patch plan. Include workers, scheduled jobs, CI runners and standalone tools that bundle a runtime. A list of public websites alone will miss part of the estate.
Inside a Node.js environment, a useful starting check is:
node -p 'JSON.stringify({node: process.version, executable: process.execPath})'
Run it inside the workload's environment. A version from an administrator's shell may come from a different binary or installation.
For an application you build into a container, update the base runtime to a fixed release, rebuild the image, deploy it and replace the old containers. Pulling an image into a registry or onto a host does not prove that the running workload changed.
For example, after updating the Dockerfile for a Compose service named app:
docker compose build --pull app
docker compose up -d --no-deps --force-recreate app
docker compose exec app node --version
That example assumes the service is built from your Dockerfile. If you consume a vendor image, deploy its confirmed fixed build instead.
These commands check the executable available in that environment. Also confirm that the application process was replaced and reports the expected runtime at startup; a new version-check process does not prove an older worker has exited.
For Kubernetes, update the workload's image reference, follow the rollout and verify all replicas. Include CronJob templates and other controllers that could launch the previous image later. Keep rollback artefacts under review so an incident rollback does not quietly restore the vulnerable runtime.
Managed hosting needs the same evidence at a different boundary. Confirm the provider's fixed runtime/build and establish whether existing applications need a redeployment. “The platform has a patch” and “our workload is running it” are separate checks.
What developers need to do
Developers can identify where untrusted data reaches decompression, including behaviour supplied by frameworks and HTTP clients.
I would review body parsers, outbound URL fetches, file imports, queue consumers and explicit Brotli calls. Search results help find candidates, but they will not reveal every transitive dependency or bundled native library.
If the application does not need compressed request bodies, disable inflation in the parsers it already uses. For an existing Express JSON parser, that can mean:
app.use(express.json({ inflate: false }));
This rejects compressed bodies for requests handled by that parser. It also rejects gzip and deflate, so check legitimate clients before changing the setting. Apply the decision consistently to other relevant parsers; changing one JSON parser does not disable a separate raw parser or a custom decompression call. body-parser options.
An earlier gateway policy can provide another rejection point. Reject the unsupported request rather than removing its encoding header and forwarding compressed bytes under a misleading description.
For outbound HTTP clients, review the actual decompression behaviour. Sending Accept-Encoding: identity is a request to the remote server; it is not proof that a malicious server cannot return Brotli data or that the client will refuse to decode it. Until patched, any temporary restriction has to prevent untrusted data from reaching the decoder.
Also test the application after updating the runtime: authentication, uploads, integrations, background jobs and normal compressed traffic where it remains supported. The patch needs to reach production without leaving a broken business workflow that encourages an unsafe rollback.
How I would verify the fix
I would keep deployment verification and exposure testing separate.
First, verify the Node.js release or vendor-fixed package in every running instance, along with the deployed image digest and replacement of old processes.
Be careful with process.versions.brotli. Node.js 24.22.1 cherry-picked the security patch while retaining the bundled Brotli version string of 1.2.0. That string alone cannot distinguish this fixed build from a vulnerable one. Node.js release notes, vendored version header.
Then verify temporary controls in staging using an ordinary, valid Brotli-compressed test body. Check whether the gateway rejects it, whether the backend sees it and which parser handles it. Exercise relevant routes and direct-origin restrictions.
A valid compressed request tests routing and policy behaviour. It does not prove that a decoder is patched. I would use release and build evidence for that, and avoid sending a public exploit to production as a health check.
What if the service may already have been targeted?
Patching removes the vulnerability from the updated component. It does not tell us what happened before the update.
Given ELLIO's report of exploit traffic, I would preserve and review relevant gateway, WAF, application and process telemetry for exposed systems. 26 October update.
Useful leads include unusual Brotli requests, decoder failures, repeated worker crashes, unexpected child processes, new files and unexplained network activity. These are investigation leads, not a reliable signature on their own. Ordinary applications can use Brotli, and a failed exploit may resemble an application crash.
Where there are signs of compromise, follow the incident-response process: preserve evidence, isolate affected workloads as appropriate, assess credentials available to the process and rebuild from trusted artefacts. Credential rotation should follow the assessed exposure. A clean vulnerability scan after patching does not answer those incident questions.
Final thoughts
I want runtime patching to be something we can complete from a service inventory, without having to reconstruct every application's deployment during an urgent security update. Node2Shell is a useful case for checking whether our records include bundled runtimes and the people responsible for updating them.
If we need a temporary gateway or WAF rule, it should have an owner and a review date tied to the runtime update.
Once the fixed deployment is verified, I would check the image templates, scheduled workloads and rollback versions that could bring the old decoder back.
