# HTTP2Shell: The Brotli Bug That Reached Almost Every HTTP Stack

URL: https://nanosolutions.io/http2shell-brotli-cve-2026-111497
Date: 1792800000
Description: A pre-auth RCE in Brotli's decoder went from Node.js to Python and now HTTP stacks. Check your versions and see what to disable today.

---


<div class="post-tldr">
  <p class="post-tldr__label">The short version</p>
  <ul>
    <li>CVE-2026-111497 is a critical out-of-bounds write in Google's Brotli decoder, versions 1.0.3 to 1.2.0. CVSS 9.2. A crafted compressed stream can leak memory, crash a process or run code, with no login.</li>
    <li>It started as “node2shell” in Node.js on 21 October, reached Python packages on the 22nd, and became “HTTP2Shell” across HTTP stacks on the 23rd.</li>
    <li>No attacks reported as of 26 October, and researchers say the exploit is hard to get working. Patch while that lasts.</li>
    <li>Fixed: Node.js 22.23.4, 24.22.1, 26.12.1; Brotli 1.2.1 (or 1.0.10 / 1.1.1 on older lines); Python Brotli 1.2.1 and brotlicffi 1.2.1.0; Bun 1.4.4; .NET 8.0.33, 9.0.22, 10.0.14; Envoy and Istio patch releases. Node.js 18 and 20 get no fix.</li>
    <li>If you cannot patch today, stop decoding Brotli: reject incoming <code>Content-Encoding: br</code> and remove <code>br</code> from outbound <code>Accept-Encoding</code>.</li>
  </ul>
</div>

Aikido Security named it “HTTP2Shell” on 23 October, when a Brotli bug disclosed in Node.js two days earlier spread to a much wider set of HTTP software.

OpenAI found the bug in February, and it was fixed in Brotli's source code then. Nobody flagged it as a security problem, so no release shipped the fix until October. Now every project that bundles Brotli has to ship its own update: runtimes, libraries, proxies, appliances and container images.

## Check your stack

Pick what you run, then the version. You get a verdict and the next thing to do. It runs in your browser and sends nothing anywhere.

<div x-data="http2shell()" class="not-prose my-10 rounded-sm border border-gray-200 bg-[#f7f7f7] p-5 sm:p-8">
  <p class="text-xs font-semibold uppercase tracking-wider text-brand-accent">HTTP2Shell checker</p>
  <h3 class="mt-1 font-heading text-2xl font-bold text-[#303133]">Are you exposed?</h3>
  <p class="mt-2 text-sm text-gray-600">Two taps. Based on the fixed releases published 21 to 26 October 2026.</p>
  <div class="mt-6" role="group" aria-labelledby="h2s-step1">
    <p id="h2s-step1" class="flex items-center gap-2 font-heading text-base font-semibold text-[#303133]"><span class="inline-flex h-6 w-6 items-center justify-center rounded-sm bg-[#303133] text-xs text-white" aria-hidden="true">1</span>What do you run?</p>
    <div class="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-5">
      <template x-for="s in stacks" :key="s.id">
        <button type="button" x-on:click="pickStack(s.id)" :aria-pressed="(stack === s.id).toString()" class="min-h-12 rounded-sm border border-gray-300 bg-white px-2 py-3 text-center text-sm font-semibold text-[#303133] transition-colors hover:border-brand-accent hover:text-brand-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-accent aria-pressed:border-brand-accent aria-pressed:bg-brand-accent aria-pressed:text-white" x-text="s.label"></button>
      </template>
    </div>
  </div>
  <div x-show="current && current.options" x-cloak x-ref="step2wrap" class="mt-7" role="group" aria-labelledby="h2s-step2" x-transition:enter="motion-safe:transition motion-safe:duration-200 motion-safe:ease-out" x-transition:enter-start="opacity-0 motion-safe:translate-y-1" x-transition:enter-end="opacity-100 translate-y-0">
    <p id="h2s-step2" x-ref="step2" tabindex="-1" class="flex items-center gap-2 font-heading text-base font-semibold text-[#303133] outline-none"><span class="inline-flex h-6 w-6 items-center justify-center rounded-sm bg-[#303133] text-xs text-white" aria-hidden="true">2</span><span x-text="current ? current.question : ''"></span></p>
    <p x-show="current && current.cmd" class="mt-1 text-sm text-gray-600">Find out with <code class="rounded-sm bg-white px-1.5 py-0.5 font-mono text-[13px] text-[#303133] ring-1 ring-gray-200" x-text="current ? current.cmd : ''"></code></p>
    <div class="mt-3 grid grid-cols-2 gap-2">
      <template x-for="(o, i) in (current ? current.options || [] : [])" :key="stack + i">
        <button type="button" x-on:click="pickOption(i)" :aria-pressed="(choice === i).toString()" class="min-h-12 rounded-sm border border-gray-300 bg-white px-3 py-2.5 text-left text-sm font-medium text-[#303133] transition-colors hover:border-brand-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-accent aria-pressed:border-brand-accent aria-pressed:bg-brand-accent aria-pressed:text-white" x-text="o.label"></button>
      </template>
    </div>
    <div x-show="canPaste" class="mt-4">
      <label for="h2s-paste" class="text-sm font-semibold text-[#303133]">Or paste what <code class="font-mono text-[13px]" x-text="current ? current.cmd : ''"></code> prints</label>
      <input id="h2s-paste" type="text" x-model="pasted" x-on:input="choice = null; $nextTick(() => settle())" :placeholder="current ? 'e.g. ' + current.example : ''" autocomplete="off" autocapitalize="off" spellcheck="false" class="mt-1 block w-full rounded-sm border border-gray-300 bg-white px-3 py-2.5 font-mono text-base text-[#303133] placeholder:text-gray-400 focus:border-brand-accent focus:outline-none focus:ring-1 focus:ring-brand-accent sm:text-sm">
      <p x-show="pasted.trim() && !pastedVersion" class="mt-1 text-xs text-gray-600">That does not look like a version number yet. Try something like <span class="font-mono" x-text="current ? current.example : ''"></span>.</p>
    </div>
  </div>
  <div x-ref="result" aria-live="polite">
    <div x-show="verdict" x-cloak x-ref="card" class="mt-7 rounded-sm border-l-4 p-5 sm:p-6" :class="tone.box">
      <div class="flex items-start gap-4">
        <span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-sm text-xl font-bold text-white" :class="tone.badge" x-text="tone.icon" aria-hidden="true"></span>
        <div class="min-w-0">
          <p class="text-xs font-semibold uppercase tracking-wider" :class="tone.kicker" x-text="card.kicker || tone.label"></p>
          <p class="mt-0.5 font-heading text-xl font-bold leading-snug text-[#303133] sm:text-2xl" x-text="card.title"></p>
        </div>
      </div>
      <p class="mt-3 text-[15px] leading-relaxed text-gray-700" x-text="card.body"></p>
      <div x-show="card.cmd" class="mt-4">
        <div class="flex items-end justify-between gap-2">
          <p class="text-xs font-semibold uppercase tracking-wider text-gray-600" x-text="card.cmdLabel || 'Run'"></p>
          <button type="button" x-on:click="copy()" class="rounded-sm border border-gray-300 bg-white px-2.5 py-1 text-xs font-semibold text-[#303133] transition-colors hover:border-brand-accent hover:text-brand-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-accent" x-text="copied ? 'Copied' : 'Copy'"></button>
        </div>
        <pre class="mt-1.5 whitespace-pre-wrap break-words rounded-sm bg-[#141618] p-3 font-mono text-[13px] leading-relaxed text-gray-100" x-text="card.cmd"></pre>
      </div>
      <div x-show="card.tone !== 'good'" class="mt-4 border-t border-black/10 pt-4 text-sm leading-relaxed text-gray-700">
        <p><strong class="text-[#303133]">Until it is patched:</strong> stop decoding Brotli. Reject requests with <code class="whitespace-nowrap font-mono text-[13px]">Content-Encoding: br</code> and drop <code class="whitespace-nowrap font-mono text-[13px]">br</code> from outbound <code class="whitespace-nowrap font-mono text-[13px]">Accept-Encoding</code>.</p>
      </div>
    </div>
  </div>
  <div x-show="stack" x-cloak class="mt-5">
    <button type="button" x-on:click="reset()" class="text-sm font-semibold text-[#303133] underline decoration-gray-300 underline-offset-4 hover:text-brand-accent hover:decoration-brand-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-accent">Check another part of your stack</button>
  </div>
</div>

<script>
function http2shell() {
  const TONES = {
    good: { box: 'border-emerald-600 bg-emerald-50', badge: 'bg-emerald-600', kicker: 'text-emerald-700', icon: '✓', label: 'Fixed' },
    warn: { box: 'border-amber-500 bg-amber-50', badge: 'bg-amber-500', kicker: 'text-amber-800', icon: '!', label: 'Update needed' },
    bad: { box: 'border-red-600 bg-red-50', badge: 'bg-red-600', kicker: 'text-red-700', icon: '✕', label: 'No fix for this version' },
    check: { box: 'border-gray-400 bg-white', badge: 'bg-[#303133]', kicker: 'text-gray-600', icon: '?', label: 'Check first' }
  };
  const REBUILD = 'Confirm the version that is actually running, and rebuild any container image that bundles an older copy.';
  const parse = (s) => s.split('.').map(Number);
  const atLeast = (v, min) => {
    for (let i = 0; i < min.length; i++) {
      const a = v[i] || 0;
      if (a !== min[i]) return a > min[i];
    }
    return true;
  };
  const fixed = (extra) => ({ tone: 'good', title: "You're fixed", body: 'This release has the fix. ' + REBUILD + (extra ? ' ' + extra : '') });
  const newer = (name) => ({ tone: 'check', title: 'Newer than the advisory', body: 'This ' + name + ' release came out after the advisory, so it is not listed. Check that its release notes mention CVE-2026-111497.' });
  const debian = { tone: 'warn', title: 'Update libbrotli1', body: "Debian and Ubuntu have shipped fixed packages. Update, then restart the services that load the library, or reboot. On Ubuntu 22.04 and later this also fixes Ubuntu's own Node.js package.", cmd: 'sudo apt update && sudo apt install --only-upgrade libbrotli1' };
  const STACKS = [
    {
      id: 'node', label: 'Node.js', question: 'Which Node.js version?', cmd: 'node -v', example: 'v24.21.0',
      options: [
        { label: '26.12.1 or later', v: [26, 12, 1] },
        { label: 'Older 26.x', v: [26, 0, 0] },
        { label: '24.22.1 or later', v: [24, 22, 1] },
        { label: 'Older 24.x', v: [24, 0, 0] },
        { label: '22.23.4 or later', v: [22, 23, 4] },
        { label: 'Older 22.x', v: [22, 0, 0] },
        { label: '18.x or 20.x', v: [20, 0, 0] },
        { label: "Ubuntu apt package", verdict: debian },
        { label: 'Not sure', verdict: { tone: 'check', title: 'Find the version first', body: 'Run this on the server, or inside the container, and paste the output above.', cmd: 'node -v' } }
      ],
      judge(v) {
        const fix = { 22: '22.23.4', 24: '24.22.1', 26: '26.12.1' }[v[0]];
        if (fix) {
          if (atLeast(v, parse(fix))) return fixed();
          return { tone: 'warn', title: 'Update to ' + fix, body: 'Install Node.js ' + fix + ' or later, restart the service, then check node -v. On Docker, rebuild with docker build --pull so you get the patched node:' + v[0] + ' image.', cmd: 'nvm install ' + fix };
        }
        if (v[0] > 26) return newer('Node.js');
        return { tone: 'bad', title: 'No fix coming. Upgrade', body: 'Node.js ' + v[0] + ' is out of support and will not get a patch. Move to 22.23.4, 24.22.1 or 26.12.1.', cmd: 'nvm install 24.22.1' };
      }
    },
    {
      id: 'python', label: 'Python', question: 'Which Brotli package?', cmd: 'pip show Brotli brotlicffi',
      options: [
        { label: 'Brotli 1.2.1 or later', verdict: fixed('Regenerate the lockfile so the next deploy cannot roll it back.') },
        { label: 'Brotli older than 1.2.1', verdict: { tone: 'warn', title: 'Update Brotli to 1.2.1', body: 'Upgrade, regenerate the lockfile, rebuild and restart. If you do not need Brotli, uninstall it instead: Requests, HTTPX and urllib3 fall back to gzip and deflate.', cmd: 'pip install -U "Brotli>=1.2.1"' } },
        { label: 'brotlicffi 1.2.1.0 or later', verdict: fixed('Regenerate the lockfile so the next deploy cannot roll it back.') },
        { label: 'brotlicffi older than 1.2.1.0', verdict: { tone: 'warn', title: 'Update brotlicffi to 1.2.1.0', body: 'Upgrade, regenerate the lockfile, rebuild and restart. If you do not need Brotli, uninstall it instead: Requests, HTTPX and urllib3 fall back to gzip and deflate.', cmd: 'pip install -U "brotlicffi>=1.2.1.0"' } },
        { label: 'Neither is installed', verdict: { tone: 'good', kicker: 'Not through Python', title: 'Your Python packages are clear', body: 'No Python Brotli package means Python is not decoding Brotli. A runtime, an OS package or a proxy on the same server still might.' } },
        { label: 'Not sure', verdict: { tone: 'check', title: 'Check each environment', body: 'Run this in every virtualenv and container. It prints the version of each package that is installed. Do not rely on pip-audit or Dependabot alone: they missed these packages for the first few days.', cmd: 'pip show Brotli brotlicffi' } }
      ]
    },
    {
      id: 'dotnet', label: '.NET', question: 'Which .NET runtime?', cmd: 'dotnet --list-runtimes', example: 'Microsoft.NETCore.App 8.0.32',
      options: [
        { label: '10.0.14 or later', v: [10, 0, 14] },
        { label: 'Older 10.0', v: [10, 0, 0] },
        { label: '9.0.22 or later', v: [9, 0, 22] },
        { label: 'Older 9.0', v: [9, 0, 0] },
        { label: '8.0.33 or later', v: [8, 0, 33] },
        { label: 'Older 8.0', v: [8, 0, 0] },
        { label: '7.0 or older', v: [7, 0, 0] },
        { label: 'Not sure', verdict: { tone: 'check', title: 'Find the version first', body: 'Run this on the server and paste the output above. Self-contained apps carry their own runtime, so check those builds too.', cmd: 'dotnet --list-runtimes' } }
      ],
      judge(v) {
        const fix = { 8: '8.0.33', 9: '9.0.22', 10: '10.0.14' }[v[0]];
        if (fix) {
          if (atLeast(v, parse(fix))) return fixed('Self-contained apps carry their own runtime, so republish those.');
          return { tone: 'warn', title: 'Update to ' + fix, body: 'Install the ' + fix + ' runtime (Microsoft Update carries it on Windows), republish self-contained apps and rebuild containers. Microsoft also lists PowerShell and Visual Studio updates for this CVE.', cmdLabel: 'Check afterwards', cmd: 'dotnet --list-runtimes' };
        }
        if (v[0] > 10) return newer('.NET');
        return { tone: 'bad', title: 'No fix coming. Upgrade', body: '.NET ' + v[0] + ' is out of support and will not get a patch. Move to .NET 8.0.33, 9.0.22 or 10.0.14.' };
      }
    },
    {
      id: 'java', label: 'Java', question: 'Which brotli4j version?', cmd: 'mvn dependency:tree | grep brotli4j', example: '1.23.0',
      options: [
        { label: '1.24.0 or later', v: [1, 24, 0] },
        { label: 'Older than 1.24.0', v: [1, 23, 0] },
        { label: 'Not sure', verdict: { tone: 'check', title: 'Check the dependency tree', body: 'Run this in each project (or gradle dependencies on Gradle). brotli4j often arrives through Netty or an HTTP client rather than directly.', cmd: 'mvn dependency:tree | grep brotli4j' } }
      ],
      judge(v) {
        if (atLeast(v, [1, 24, 0])) return fixed('Keep the native-* artifacts on the same version.');
        return { tone: 'warn', title: 'Update to 1.24.0', body: 'Bump brotli4j and its native-* artifacts to 1.24.0 or later, then rebuild and redeploy.', cmdLabel: 'Set the dependency to', cmd: 'com.aayushatharva.brotli4j:brotli4j:1.24.0' };
      }
    },
    {
      id: 'bun', label: 'Bun', question: 'Which Bun version?', cmd: 'bun --version', example: '1.4.3',
      options: [
        { label: '1.4.4 or later', v: [1, 4, 4] },
        { label: '1.4.3 or older', v: [1, 4, 3] },
        { label: 'Not sure', verdict: { tone: 'check', title: 'Find the version first', body: 'Run this where the app runs and paste the output above.', cmd: 'bun --version' } }
      ],
      judge(v) {
        if (atLeast(v, [1, 4, 4])) return fixed();
        return { tone: 'warn', title: 'Update to 1.4.4', body: 'Upgrade Bun, restart the app, and rebuild any image or compiled executable made with the old version.', cmd: 'bun upgrade' };
      }
    },
    {
      id: 'envoy', label: 'Envoy', question: 'Which Envoy version?', cmd: 'envoy --version', example: '1.38.2',
      options: [
        { label: '1.40.0 or later', v: [1, 40, 0] },
        { label: '1.36 or older', v: [1, 36, 0] },
        { label: '1.39.3 or later', v: [1, 39, 3] },
        { label: 'Older 1.39.x', v: [1, 39, 0] },
        { label: '1.38.6 or later', v: [1, 38, 6] },
        { label: 'Older 1.38.x', v: [1, 38, 0] },
        { label: '1.37.8 or later', v: [1, 37, 8] },
        { label: 'Older 1.37.x', v: [1, 37, 0] },
        { label: 'Not sure', verdict: { tone: 'check', title: 'Find the version first', body: 'Run this on one of the proxies, or check the image tag, and paste the output above.', cmd: 'envoy --version' } }
      ],
      judge(v) {
        if (v[0] !== 1) return newer('Envoy');
        if (v[1] === 40) return fixed();
        if (v[1] > 40) return newer('Envoy');
        const fix = { 39: '1.39.3', 38: '1.38.6', 37: '1.37.8' }[v[1]];
        if (fix) {
          if (atLeast(v, parse(fix))) return fixed();
          return { tone: 'warn', title: 'Update to ' + fix, body: 'Move to Envoy ' + fix + ' or later, roll every proxy, then check envoy --version on each one.', cmdLabel: 'Pull the fixed image', cmd: 'docker pull envoyproxy/envoy:v' + fix };
        }
        return { tone: 'bad', title: 'No fix coming. Upgrade', body: 'Envoy 1.' + v[1] + ' will not get a patch. Move to 1.40.0, or to 1.39.3, 1.38.6 or 1.37.8.', cmdLabel: 'Pull a fixed image', cmd: 'docker pull envoyproxy/envoy:v1.40.0' };
      }
    },
    {
      id: 'istio', label: 'Istio', question: 'Which Istio version?', cmd: 'istioctl version', example: '1.30.2',
      options: [
        { label: '1.31.2 or later', v: [1, 31, 2] },
        { label: 'Older 1.31.x', v: [1, 31, 0] },
        { label: '1.30.6 or later', v: [1, 30, 6] },
        { label: 'Older 1.30.x', v: [1, 30, 0] },
        { label: '1.29.9 or later', v: [1, 29, 9] },
        { label: 'Older 1.29.x', v: [1, 29, 0] },
        { label: '1.28 or older', v: [1, 28, 0] },
        { label: 'Not sure', verdict: { tone: 'check', title: 'Find the version first', body: 'This shows the control plane and data plane versions. Paste the output above.', cmd: 'istioctl version' } }
      ],
      judge(v) {
        if (v[0] !== 1 || v[1] > 31) return newer('Istio');
        const restart = 'Sidecars keep the old proxy until their pods restart, so restart every workload after the upgrade.';
        const fix = { 31: '1.31.2', 30: '1.30.6', 29: '1.29.9' }[v[1]];
        if (fix) {
          if (atLeast(v, parse(fix))) return Object.assign(fixed(restart), { cmdLabel: 'Restart workloads', cmd: 'kubectl rollout restart deployment -n your-namespace' });
          return { tone: 'warn', title: 'Update to ' + fix, body: 'Upgrade the control plane and gateways to Istio ' + fix + '. ' + restart, cmdLabel: 'Then restart workloads', cmd: 'kubectl rollout restart deployment -n your-namespace' };
        }
        return { tone: 'bad', title: 'No fix coming. Upgrade', body: 'Istio 1.' + v[1] + ' will not get a patch. Move to 1.31.2, 1.30.6 or 1.29.9. ' + restart };
      }
    },
    {
      id: 'linux', label: 'Linux server', question: 'Which distribution?', cmd: 'cat /etc/os-release',
      options: [
        { label: 'Debian or Ubuntu', verdict: debian },
        { label: 'Alpine', verdict: { tone: 'warn', title: 'Update brotli-libs', body: 'Alpine has shipped fixed packages. In a container, rebuild from a fresh base image (docker build --pull) rather than patching a running one.', cmd: 'apk upgrade --no-cache brotli-libs' } },
        { label: 'Amazon Linux', verdict: { tone: 'warn', title: 'Update brotli', body: 'Amazon Linux has shipped fixed packages. Use yum instead of dnf on Amazon Linux 2. Restart the services that load the library, or reboot.', cmd: 'sudo dnf upgrade brotli' } },
        { label: 'Red Hat', verdict: { tone: 'bad', kicker: 'No fixed package yet', title: 'Stop decoding Brotli for now', body: 'On 26 October the standard Red Hat brotli package had no fix. Turn off Brotli decoding on these servers and watch Red Hat’s errata for the update.', cmdLabel: 'Check the installed version', cmd: 'rpm -q brotli' } },
        { label: 'SUSE', verdict: { tone: 'bad', kicker: 'No fixed package yet', title: 'Stop decoding Brotli for now', body: 'On 26 October the standard SUSE brotli package had no fix. Turn off Brotli decoding on these servers and watch SUSE’s security updates.', cmdLabel: 'Check the installed version', cmd: 'rpm -q libbrotlidec1' } }
      ]
    },
    {
      id: 'web', label: 'nginx / Apache',
      verdict: { tone: 'good', kicker: 'Not affected by default', title: 'The web server is fine. Check the app behind it', body: "Apache's mod_brotli and nginx's Brotli module only compress responses. They do not decode Brotli. A Node, Python or Go app behind them that decompresses request bodies is still exposed, so pick that app above." }
    },
    {
      id: 'unsure', label: 'Not sure',
      verdict: { tone: 'check', title: 'Start with an inventory', body: 'Run these where your apps run, including inside containers, then pick what turns up above. The last line is for Debian or Ubuntu. On managed hosting or a CDN, ask the provider which Brotli version it runs.', cmd: 'node -v\npip show Brotli brotlicffi\ndotnet --list-runtimes\ndpkg -l libbrotli1' }
    }
  ];
  return {
    stacks: STACKS,
    stack: null,
    choice: null,
    pasted: '',
    copied: false,
    get current() { return STACKS.find((s) => s.id === this.stack) || null; },
    get canPaste() { return !!(this.current && this.current.judge); },
    get pastedVersion() {
      const m = this.pasted.match(/(\d+)\.(\d+)(?:\.(\d+))?/);
      return m ? [Number(m[1]), Number(m[2]), Number(m[3] || 0)] : null;
    },
    get verdict() {
      const s = this.current;
      if (!s) return null;
      if (!s.options) return s.verdict;
      if (s.judge && this.pastedVersion) return s.judge(this.pastedVersion);
      if (this.choice === null) return null;
      const o = s.options[this.choice];
      return o.v ? s.judge(o.v) : o.verdict;
    },
    get card() { return this.verdict || { tone: 'check', title: '', body: '' }; },
    get tone() { return TONES[this.card.tone]; },
    calm() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; },
    reveal(el) { if (el) el.scrollIntoView({ block: 'nearest', behavior: this.calm() ? 'auto' : 'smooth' }); },
    pop() {
      const el = this.$refs.card;
      if (!el || this.calm() || !el.animate) return;
      el.animate([{ opacity: 0, transform: 'translateY(6px) scale(0.99)' }, { opacity: 1, transform: 'none' }], { duration: 240, easing: 'cubic-bezier(.2,.8,.2,1)' });
    },
    settle() { this.copied = false; if (this.verdict) this.pop(); },
    pickStack(id) {
      this.stack = id;
      this.choice = null;
      this.pasted = '';
      this.copied = false;
      this.$nextTick(() => {
        if (this.current.options) {
          this.$refs.step2.focus({ preventScroll: true });
          this.reveal(this.$refs.step2wrap);
        } else {
          this.pop();
          this.reveal(this.$refs.result);
        }
      });
    },
    pickOption(i) {
      this.choice = i;
      this.pasted = '';
      this.$nextTick(() => { this.settle(); this.reveal(this.$refs.result); });
    },
    copy() {
      if (!navigator.clipboard) return;
      navigator.clipboard.writeText(this.card.cmd).then(() => {
        this.copied = true;
        setTimeout(() => { this.copied = false; }, 1600);
      }).catch(() => {});
    },
    reset() {
      this.stack = null;
      this.choice = null;
      this.pasted = '';
      this.copied = false;
      this.$nextTick(() => this.$root.querySelector('button').focus());
    }
  };
}
</script>

## What went wrong

Brotli is Google's compression format for web traffic. Its decoder ships inside Node.js, Python packages, Bun, .NET, Envoy, curl, CDNs and API gateways, usually as a dependency nobody chose.

In versions 1.0.3 to 1.2.0, a crafted stream makes the decoder write past the end of a small buffer. That can leak memory, crash the process or let an attacker run code. It works both ways. A malicious client can attack a server that decodes request bodies, and a malicious server can attack a client that decodes responses. No login is needed. NVD lists Google's CVSS v4.0 score of 9.2, critical. Node.js rates it high.

<figure>
  <img src="/images/blog/http2shell-network-switch.webp" alt="A Raspberry Pi 4 board sitting on top of a network switch" width="1400" height="930" loading="lazy" />
  <figcaption>Brotli is usually not a deliberate choice; it arrives inside a runtime, a proxy or a client library. Photo by <a href="https://commons.wikimedia.org/w/index.php?curid=159094209" rel="nofollow noopener">RAIL P on Wikimedia Commons</a>.</figcaption>
</figure>

## Fixed versions

From the advisories published between 21 and 26 October:

| Project | Fixed release |
|---|---|
| Node.js | 22.23.4, 24.22.1, 26.12.1. Node.js 18 and 20 get no fix |
| Google Brotli (upstream) | 1.2.1; patched 1.0.10 and 1.1.1 for older lines |
| Python `Brotli` | 1.2.1 |
| Python `brotlicffi` | 1.2.1.0 |
| Bun | 1.4.4 |
| Envoy | 1.40.0, 1.39.3, 1.38.6, 1.37.8 |
| Istio | 1.29.9, 1.30.6, 1.31.2 |
| .NET | 8.0.33, 9.0.22, 10.0.14 (25 October). .NET 7 gets no fix |
| Java `brotli4j` | 1.24.0 |
| curl / libcurl | Update its Brotli dependency, or use the vendor's fixed package |
| Linux `brotli` packages | Fixed in Debian, Ubuntu, Alpine and Amazon Linux. Standard Red Hat and SUSE packages had no fix on 26 October |

On Ubuntu 22.04 and later, Ubuntu's own Node.js package uses the system Brotli library, so updating the OS package is the fix there. Containers and compiled binaries need a rebuild as well. A patched host does not patch an image that bundles the old library.

## Is it being used in attacks?

Not that anyone has reported. As of 26 October, [Rapid7](https://www.rapid7.com/blog/post/etr-cve-2026-111497-critical-out-of-bounds-write-in-the-node-js-brotli-decoder/) knew of no attacks in the wild, and the flaw is not on CISA's list of known exploited vulnerabilities.

Imperva's red team did run its own code on the official Node.js 24.22.0 image, after several attempts, and has not released the exploit. Public proof-of-concept code mostly crashes the process or leaks a little memory on a standard build ([Sysdig](https://www.sysdig.com/blog/node2shell-cve-2026-111497-detecting-the-node-js-brotli-rce)).

Security researcher s1r1us of Hacktron thinks the same thing is holding attackers back here as with [PHP2Shell](/php2shell-critical-php-vulnerabilities): the exploit is hard to adapt from one target to the next. In [their PHP2Shell test](https://x.com/S1r1u5_/status/2114774529814007865), only the most capable commercial AI models managed it; the freely downloadable ones did not. s1r1us also warns the gap may not last, as open models are improving fast. Use the quiet to patch.

## If you cannot patch today

Stop decoding Brotli until the components are fixed.

- **Incoming requests:** at the proxy, load balancer or middleware, reject any request whose `Content-Encoding` includes `br`. Match case-insensitively and handle lists of encodings.
- **Outbound requests:** remove `br` from `Accept-Encoding` and turn off automatic decoding. A malicious server can send Brotli anyway, and libraries such as urllib3 pick the decoder from the response header.
- **Python:** if Brotli is optional, uninstall `Brotli` and `brotlicffi`, remove extras such as `httpx[brotli]`, regenerate the lockfile and restart.
- **Managed hosting or CDN:** ask the provider which Brotli version it runs and when it was patched.

<figure>
  <img src="/images/blog/http2shell-lock-chain.webp" alt="A padlock and a heavy security chain" width="1400" height="934" loading="lazy" />
  <figcaption>A workaround is a temporary lock, not a fix. Get the patched component onto the device. Photo by <a href="https://wordpress.org/photos/photo/83461f065c/" rel="nofollow noopener">Jennifer Bourn on WordPress Photo Directory</a>.</figcaption>
</figure>

## Frequently asked questions

### Are we safe because we use nginx or Apache?

Not automatically. Their standard Brotli modules only compress responses, so the web server itself is fine. A Node, Python or Go app behind it that decompresses request bodies is still exposed.

### Does a clean scan mean we are patched?

No. For the first few days the CVE was not mapped to the PyPI packages, so `pip-audit` and Dependabot missed a vulnerable `Brotli` or `brotlicffi`. Aikido reported on 26 October that both flag them now, but a scanner with an out-of-date database can still miss it. Check lockfiles and installed versions directly.

## Sources

- [Aikido: Node2Shell evolves to HTTP2Shell (CVE-2026-111497)](https://www.aikido.dev/blog/python-packages-vulnerable-node2shell-brotli-cve-2026-111497). 23 October 2026, updated 26 October. Timeline, affected projects, fixed releases, scanner coverage and the Apache note.
- [NVD: CVE-2026-111497](https://nvd.nist.gov/vuln/detail/CVE-2026-111497). Published 22 October 2026.
- [Node.js October 2026 security releases](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases#nodejs-brotli-decoder-out-of-bounds-write-permits-remote-code-execution-cve-2026-111497---high). The Node.js advisory.
- [GitHub advisory GHSA-9v8h-qqgm-4m35](https://github.com/advisories/GHSA-9v8h-qqgm-4m35)
- [Aikido on X (@AikidoSecurity)](https://x.com/AikidoSecurity/status/2113759110625906950). 23 October 2026. The “HTTP2Shell” announcement.
- [Microsoft Security Update Guide: CVE-2026-111497](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-111497). 25 October 2026. .NET, PowerShell and Visual Studio updates.
- [Ubuntu](https://ubuntu.com/security/CVE-2026-111497) and [Debian](https://security-tracker.debian.org/tracker/CVE-2026-111497) security trackers. Fixed `brotli` packages.
- [ngx_brotli](https://github.com/google/ngx_brotli). Google's nginx module, which compresses responses and serves pre-compressed files.
- [Rapid7: CVE-2026-111497](https://www.rapid7.com/blog/post/etr-cve-2026-111497-critical-out-of-bounds-write-in-the-node-js-brotli-decoder/). 26 October 2026. No known attacks in the wild; not on CISA's KEV list.
- [Sysdig: Detecting the Node.js Brotli RCE](https://www.sysdig.com/blog/node2shell-cve-2026-111497-detecting-the-node-js-brotli-rce). 24 October 2026. Imperva's result and the state of public exploit code.
- [s1r1us on X](https://x.com/S1r1u5_/status/2114774529814007865). 26 October 2026. Which AI models could port the PHP2Shell exploit, and why exploitation may be held back.

## Worried about a gap?

If you would like a second pair of eyes on your stack, [talk to us](/contact). We help Perth businesses inventory dependencies, patch runtimes and rebuild images without the guesswork.

**Related reading:** [PHP2Shell: Two Critical PHP Flaws](/php2shell-critical-php-vulnerabilities) and [The eMudhra Certificate Authority Compromise](/emudhra-certificate-authority-key-compromise).

---

**Photo credits.** All photos are CC0 1.0 (public domain dedication), from Wikimedia Commons and the WordPress Photo Directory:

- Server racks at NERSC — [Derrick Coetzee](https://commons.wikimedia.org/w/index.php?curid=17445617)
- Raspberry Pi on a network switch — [RAIL P](https://commons.wikimedia.org/w/index.php?curid=159094209)
- Padlock and chain — [Jennifer Bourn](https://wordpress.org/photos/photo/83461f065c/)
