October 2026 has been a hard month for software. One tracker counted 15 supply-chain incidents in 17 days, and on October 21 a bug in Google’s Brotli decoder, Node2Shell (CVE-2026-111497), made any program that decompresses untrusted Brotli data a possible target. Researchers soon started calling it HTTP2Shell, because the flaw lives in the upstream library and reaches HTTP software far beyond Node.js.
Jutsu fetches URLs our customers choose. Our API, WebSocket, domain and dark-web monitors all make outbound requests to hosts we don’t control. So we stopped and audited our own stack end to end: every lockfile, every runtime, every container image, our GitHub organization and our data plane. This post is the result, in the open: what we checked, what we found, and what we changed.
The timing was not kind to us. Node2Shell landed in a week when half our team was on vacation. We still pulled through. The short version: no compromised package reached us, the urgent exposure was our runtime, the fix was in production six days after Node2Shell was disclosed, and our whole data plane is now back on supported releases.

Key takeaways
- Every Jutsu backend image ran Bun 1.3.14, which bundles the vulnerable Brotli decoder. We moved every image, CI job and build to Bun 1.4.4 and shipped it to production on October 27, six days after disclosure.
- No malicious package reached us. About 5,570 package versions checked against 2026’s attack lists and 7,447 lookups in OSV’s malware feed found nothing.
- Open advisories in our production lockfile fell from 117 (4 critical, 42 high) to 3 (2 moderate, 1 low).
- Our data plane is back on supported releases. NATS 2.14.8, ClickHouse 26.3 LTS and Vector 0.60.0 are all in production, and each upgrade was rehearsed on beta first.
Node2Shell put our monitors in the blast radius
Node2Shell is an out-of-bounds write in Brotli’s streaming decoder: a crafted stream makes it write past the end of a small internal buffer (Akamai has a walkthrough). Brotli 1.0.3 through 1.2.0 are affected, and Google, which assigned the CVE, scores it 9.2 critical under CVSS 4.0. The decoder ships inside a lot of software: Node.js, Bun, Python’s Brotli packages and Microsoft’s .NET all shipped fixes.
The path that mattered to us is mundane. Bun’s fetch() automatically decodes a response sent with Content-Encoding: br, and Bun’s advisory says every release from 1.0.21 up to 1.4.4 did that with the vulnerable decoder. We were on 1.3.14. Our monitors call fetch() against URLs our customers give us. So any customer, or anyone who controls a host a customer monitors, could have sent a crafted response straight to our workers. We treated that as the most urgent item in the audit.
What we changed:
- Bun 1.4.4 everywhere. Both Dockerfiles, docker compose, CI, the
packageManagerfield and the console build. The Dockerfile now says in a comment why 1.4.4 is the floor, so nobody pins it back. - The console build stopped installing “whatever is current”. It used to install Bun with
curl | bash, unpinned, and build with whatever Node version the build image shipped, never logged. It now pins Bun 1.4.4, installs Node 24.22.1 (from the October 21 Node.js security release, which fixed 10 CVEs including this one), logs the version, and installs from a frozen lockfile.
Patching a runtime isn’t free. Bun 1.4 checks TLS hostnames itself and refuses malformed WebSocket handshakes. Both are safer, and both changed what our WebSocket scanner reports: a certificate for the wrong host came back as a rejected chain instead of a hostname mismatch, and one test target could no longer simulate an insecure handshake. We mapped the error back and gave the test target a raw TCP front, so our checks still report “certificate does not match the host” and still catch insecure handshakes. Then we ran all 2,961 test files on the old and the new Bun side by side. Every failure on the new runtime also failed on the old one, or was a known load flake. No regressions.
If you are patching this yourself, the HTTP2Shell name is the useful one. Updating Node.js does not cover a Python service or a .NET app that decodes untrusted Brotli. Each runtime, and each library that bundles its own copy of the decoder, needs its own check.
No compromised package reached us
Black October was also a supply-chain month, with a new GhostAction wave pushing a credential-stealing security-audit.yml workflow into 345 repositories on October 8, most likely through stolen tokens, and compromised releases of LiteLLM and other tools. We compared every lockfile, our installed node_modules, the Bun cache and the global installs on our main development machine against the published package lists of 2026’s npm compromises, and against OSV’s malware feed. We also looked for the files the worms leave behind. Nothing matched.
One gap we want to be clear about: the full per-version lists for May’s “Mini Shai-Hulud” wave and August’s “ChainDrop” wave could not all be fetched, so we checked those by package name and through OSV instead.
We then swept all 85 active repositories in our GitHub organization for GhostAction’s workflow files, unexpected deploy keys, outside collaborators and pending invitations. We found none. The sweep did find a weakness of our own: every action in our workflows was referenced by a mutable tag, including the two deploy workflows that receive cloud credentials. Anyone who controls an action’s repository can move a tag to new code; a commit SHA can’t move. Every action is now pinned to the commit its tag pointed at. We also reviewed which installed GitHub Apps can write code or workflows.

117 dependency advisories down to 3
bun audit reported 117 advisories against the lockfile of our October 20 production release: 4 critical, 42 high, 60 moderate and 11 low. bun audit fix cleared 100 of them inside our existing version ranges, including everything on our API’s request path: hono, axios and follow-redirects, fast-uri, js-yaml, dompurify and mermaid.
The other 17 needed major-version upgrades, so we did those one at a time:
- zod 3 to 4. GHSA-mrhx-c7xc-m3w5: an array with a maximum length still checked every element before reporting it was too long, so an oversized request body could tie up the validator. The fix exists only in zod 4. On 200,000 items, zod 4 returned 1 issue in 1.5 ms; the zod 3 code returned 200,001 issues in 60 ms.
- drizzle-orm 0.36 to 0.45.4. A SQL identifier escaping flaw, fixed in 0.45.2. Our exposure was low, because every identifier we pass is a constant in our code, never user input. We upgraded anyway and re-applied our own connection-pool leak fix on top.
- maplibre-gl 6, react-router 7, vite 8 and happy-dom 21 for the console: a sanitizer bypass in the map library, four open-redirect advisories in the router, dev-server file access, and a test-only issue.

Three remain, all moderate or low: esbuild’s development server (developer machines only), a prototype-pollution issue in the KaTeX math renderer, and uuid inside a spreadsheet library, which needs an upstream release.
The upgrades almost opened two holes of their own
A security upgrade is a code change, and code changes break things. Two of the problems we caught were security bugs in their own right:
- zod 4 silently drops
__proto__. When zod 4 parses a record, it discards a__proto__key. A compliance check definition that contained one would validate clean while the raw object still carried it. Under zod 3 the key survived and our parameter checks refused it. We now refuse__proto__explicitly, at any depth, before zod sees the input. - A backslash beat our redirect check. While moving to react-router 7 we found that
/\evil.examplepassed our “starts with / but not //” test for the page you return to after login. Browsers read a backslash as a slash, so it behaved like//evil.example: an open redirect. Return paths now refuse backslashes and control characters, and the same check guards links that Jutsu AI renders from model output.
Others weren’t security bugs but would have hurt. zod 4 fills in default values inside partial objects, which would have made every detection-rule toggle fail with a 403 and overwrite stored fields. We built a partial schema without defaults to keep the old behavior. And zod 4’s stricter UUID check rejects the IDs of our own built-in detection rules, so 288 call sites moved to the looser GUID check.
Our data plane is back on supported releases
Three components sit under every alert Jutsu raises. ClickHouse stores the events, NATS carries work between our services, and Vector receives and normalizes logs. All three were behind, and we upgraded them one at a time, beta first, so a problem in one couldn’t hide behind another.

- ClickHouse 25.3 to 26.3 LTS. ClickHouse’s security policy lists every 25.x release as unsupported, so a future ClickHouse CVE would not be fixed for us. The CVE that first prompted the review, CVE-2026-51992, was rejected on August 6 because the behavior it describes is a PostgreSQL feature, not a ClickHouse bug. GitHub’s advisory database still lists it as unreviewed and critical, so your scanner may still flag it. Our application’s database users can’t create dictionaries or tables anyway. We upgraded to stay on a line that gets fixes.
- NATS 2.10 to 2.14.8. NATS supports only its two most recent release series, and security note 2026-28, published in June, describes a remote crash fixed only in 2.14.3 and 2.12.12. Our NATS server has never been reachable from the internet, only from inside our own cluster. We stepped it through 2.11 and 2.12 to 2.14.8, on beta first and then in production, checking every stream and consumer for an exact match at each step, and added a network policy so only our own services can reach it.
- Vector 0.56 to 0.60.0. The three Vector CVEs published in July sit in components we don’t run (the logstash source and the file sink), so this was about drift. Version 0.57 changed how config variables are filled in and limited what sink templates can reach, and four releases of changes only get harder to absorb, so we took them now.
A database upgrade is where a SIEM can lose data or mix up tenants, so most of the work went into proving each step safe before it ran. This is what we built:
- A compatibility pin. ClickHouse 26.3 first booted with its compatibility setting at 25.3. On test servers, a settings check per database user showed every user gets the same effective settings as before. Without the pin it found 29 differences.
- A two-tenant isolation probe. It sends every kind of input our aggregator handles, for two organizations, through a real aggregator into a real ClickHouse, then reads it back as the same database users production uses. A row that lands in the wrong tenant fails the probe.
- A pause switch for destructive jobs. Asset purges, retention cleanups, organization deletes and database drops now check a flag before every statement. If the flag can’t be read, they pause rather than proceed.
- Real ClickHouse in CI. Our CI used to run against an empty ClickHouse, so every suite that touched real tables sat in quarantine. CI now starts ClickHouse the way production runs it and applies all 91 migrations in about 15 seconds. That cleared 32 entries from our quarantine list.
- A dry run for NATS. Before each switch, a check script made every message-stream management call our services make at startup against the target version, on a restored copy of the real data, and compared the answers.
Not every step went cleanly. One preparatory change for the Vector upgrade made our log-ingestion pods report “not ready” whenever a destination went quiet for a minute. On beta, that blocked a rollout. We reverted it before it reached production and rebuilt it before the Vector upgrade, so a quiet destination no longer takes a pod out of service.
Provider fixes in the same release
- Railway. After its October security incident, Railway invalidated OAuth sessions on October 21 and every API token on October 22 (UTC), and older project tokens stay invalid. If you connected Railway to Jutsu before October 22, reconnect it so the audit-log pulls resume.
- Supabase. A live test showed that a wrong password was graded as a low-severity “request failed” and never reached Events on a default connection. Failed logins are now graded from the real auth log line, and a new brute-force rule watches them. We also measured Supabase storing some log rows more than two minutes after they happen, after our poller had moved on, so each poll now ends with a sweep of the last ten minutes. And unreadable API answers now fail closed: before, an empty member list could read as “every member was removed”.
- Render. New in this release: Render workspace audit logs, a 19-rule detection pack, a rule for bursts of five or more deletions in ten minutes, and four response actions, three of them reversible, such as removing open-to-the-internet entries from a database’s IP allow list.
- A ClickHouse query bug. One cloud security report query gave a converted timestamp the same name as its original column, so its time filter compared text with a date and failed on a real server. Tests against a fake database couldn’t see it, which is exactly why CI now runs a real ClickHouse.
What we would tell another team
- Inventory runtimes, not just packages. Our lockfiles were clean. Our runtime was not.
- Pin everything that installs code. Container tags, installer scripts and CI actions. A floating Redis tag meant each node kept whatever version it had pulled first.
- Test an upgrade like a feature. Run the old and new versions side by side, and expect the upgrade itself to introduce bugs.
- Prove tenant isolation with production identities. Mocks can’t tell you which tenant a row really lands in.
How Jutsu helps when the month looks like this
- Jutsu pulls audit logs from the platforms your code runs on, including Railway, Supabase and Render, and its AI agents triage what they find, escalating uncertain cases to an analyst. When a provider resets credentials or a patch window changes who touches what, the audit trail shows who did what.
- Exposure runs an external security assessment of your internet-facing services in minutes, so you see what an attacker sees before you start patching.
- For more on this month’s supply-chain fight, read how npm v12 blocks install scripts and what it misses, and our look at the Proto6 flaws in protobuf.js, another Node.js runtime risk.
The bottom line
Our dependencies were clean and our runtime wasn’t, and a security vendor that fetches URLs for its customers has to treat that as urgent. With half the team on vacation, we patched it in six days, cleared 114 of 117 advisories, and moved the data plane back onto supported releases. Clean lockfiles are not a clean stack.
FAQ
What is Node2Shell (CVE-2026-111497)?
Node2Shell is an out-of-bounds write in Google’s Brotli decompression library, versions 1.0.3 through 1.2.0, disclosed on October 21, 2026 and scored 9.2 critical (CVSS 4.0). Any program that decompresses untrusted Brotli data can be affected, including Node.js through its zlib module and Bun, whose fetch() decodes Content-Encoding: br responses automatically. Public proof-of-concept code exists. As of October 27, 2026 it was not in CISA’s Known Exploited Vulnerabilities catalog.
Is HTTP2Shell the same as Node2Shell?
Yes. Both names refer to CVE-2026-111497, and Tenable calls it Brotli2Shell. It was first called Node2Shell because Node.js shipped the first fix, and later HTTP2Shell because the bug is in the upstream Brotli library, which also ships in Python packages, .NET and other HTTP software. It has nothing to do with HTTP/2.
Which Bun and Node.js versions fix Node2Shell?
Bun 1.4.4 (Bun 1.0.21 through 1.4.3 are affected), and Node.js 22.23.4, 24.22.1 or 26.12.1. End-of-life Node.js lines get no fix, so move to a supported one. The Brotli library itself is fixed in 1.0.10, 1.1.1 and 1.2.1.
Do I need to reconnect Railway in Jutsu?
Yes, if you connected Railway before October 22, 2026. Railway invalidated OAuth sessions on October 21 and every API token on October 22, so Jutsu can’t pull your audit log until you reconnect.
Why upgrade ClickHouse if CVE-2026-51992 was rejected?
Because ClickHouse 25.3 no longer gets security fixes at all. The rejected CVE was only the prompt. Staying on 25.3 would mean that the next real ClickHouse vulnerability had no patch for us, so we moved to 26.3 LTS while nothing was on fire.
