# CVE-2026-111497 — Brotli remediation agent

**Deterministic audit, remediation and verification for CVE-2026-111497, an out-of-bounds write in the Google Brotli decoder: three shell scripts answer with a verdict, a remediation result and a verification result, and the model reads one line of each instead of exploring the host.**

<!-- md-i18n:switcher -->
**Read this in:** English · [Deutsch](README.de.md) · [中文](README.zh.md) · [हिन्दी](README.hi.md) · [Español](README.es.md) · [Русский](README.ru.md)

[![CI](https://github.com/paulmann/CVE-2026-111497/actions/workflows/ci.yml/badge.svg)](https://github.com/paulmann/CVE-2026-111497/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/paulmann/CVE-2026-111497)](https://github.com/paulmann/CVE-2026-111497/releases)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Shell: bash](https://img.shields.io/badge/shell-bash-4EAA25.svg)](#requirements)
[![Platform: Debian/Ubuntu](https://img.shields.io/badge/platform-Debian%20%7C%20Ubuntu-A80030.svg)](#requirements)

![CVE-2026-111497 — Brotli decoder remediation agent](assets/social-preview.svg)

A deterministic detector, remediator and verifier for **CVE-2026-111497** — an out-of-bounds
write in the Google Brotli decoder (`c/dec/decode.c`, CWE-787). Three shell scripts perform the
full audit and return a compact verdict; the language model orchestrates and reads one line
instead of exploring the host by hand.

## Contents

- [Why this exists](#why-this-exists)
- [Exposure model](#exposure-model)
- [Quick start](#quick-start)
- [Verdict and exit-code contract](#verdict-and-exit-code-contract)
- [What the audit covers](#what-the-audit-covers)
- [Repository layout](#repository-layout)
- [Requirements](#requirements)
- [Limitations](#limitations)
- [Documentation](#documentation)
- [Languages](#languages)
- [Contributing](#contributing)
- [License](#license)

[Wiki](https://github.com/paulmann/CVE-2026-111497/wiki) · [Changelog](CHANGELOG.md) · [Security](SECURITY.md) · [Русская версия](README.ru.md)

## Why this exists

A complete Brotli audit is not a version check. It requires answering four questions per host:
which package is installed, whether the running processes still map the vulnerable decoder,
which application paths actually decode untrusted Brotli, and whether the distro backport is the
real fix rather than an unrelated rebuild.

Answered by hand, those questions cost dozens of tool calls per host and produce prose that
cannot be re-checked. Here they are answered by scripts with a fixed exit-code contract:

| Command | Effect on the host | Output for the agent |
|---|---|---|
| `detect.sh` | read-only | `VERDICT: SAFE\|VULNERABLE\|NOT_INSTALLED` |
| `remediate.sh` | single mutation point | `REMEDIATED: <old> -> <new>` |
| `verify.sh` | read-only | `VERIFY: PASS\|FAIL` |

## Exposure model

The vulnerable code is the **decoder**, not the encoder. Services that only compress responses
with Brotli are not the attack surface; services that decompress untrusted Brotli are.

| Reachable decode path | Typical carrier |
|---|---|
| HTTP request bodies | `Content-Encoding: br` sent to the application |
| Outbound HTTP clients | `fetch()`, libcurl with `CURLOPT_ENCODING` set to empty (accept all) |
| Inbound responses from proxies | Envoy/Istio HTTP decompressors |
| Uploaded or downloaded archives | application-level Brotli streaming |
| Bundled runtimes | Node.js, Bun, Python `Brotli`/`brotlicffi`, .NET request decompression |

Vulnerable upstream releases are **1.0.3 through 1.2.0**. Fixed releases are **1.0.10, 1.1.1 and
1.2.1** (shipped 21 October 2026), plus every distribution backport that references the CVE in
its packaged changelog.

## Quick start

```bash
# 1. Deliver the scripts once (host is an SSH alias from ~/.ssh/config)
scp src/common.sh src/detect.sh src/remediate.sh src/verify.sh <host>:/root/cve-2026-111497/

# 2. Audit — read-only
ssh <host> 'bash /root/cve-2026-111497/detect.sh'          # exit 0/1/3/10

# 3. Remediate — only when the verdict is VULNERABLE
ssh <host> 'bash /root/cve-2026-111497/remediate.sh'

# 4. Verify the actual effect
ssh <host> 'bash /root/cve-2026-111497/verify.sh'          # VERIFY: PASS|FAIL
```

## Verdict and exit-code contract

| Code | Verdict | Meaning | Next step |
|---:|---|---|---|
| 0 | `SAFE` | installed release is outside the vulnerable range, or a distro backport is already applied | document and close |
| 1 | `VULNERABLE` | installed upstream release is in the vulnerable range and no backport is present | run `remediate.sh` |
| 3 | `NOT_INSTALLED` | `libbrotli1` is absent from the host | document; re-check if Brotli arrives |
| 10 | `ERROR` | precondition or invariant violated | read the message, escalate, do not force |
| 1 (`verify.sh`) | `FAIL` | remediation did not take effect | investigate before declaring closure |

`remediate.sh` stops with code 10 before touching the system when the apt candidate equals the
installed version, or when the candidate changelog does not reference `CVE-2026-111497`.

## What the audit covers

| Check | Mechanism | Why it matters |
|---|---|---|
| Installed release and upstream version | `dpkg-query`, epoch and revision stripped | distro revisions hide the upstream version |
| Applied backport | packaged `changelog.Debian.gz` naming the CVE | the authoritative signal, not the version string |
| Apt candidate | `apt-get update` then `apt-cache policy` | decides whether a supported fix exists at all |
| Live consumers | `/proc/<pid>/maps` scan for `libbrotlidec` | a patched file does not patch a running process |
| Decode surface | libcurl Brotli feature, Node/Bun/Python/PHP modules, `ngx_brotli`, `mod_brotli` | separates decode paths from compression-only ones |
| Application decode primitives | count of PHP files with `CURLOPT_ENCODING` / `CURLOPT_ACCEPT_ENCODING` | WordPress, SimplePie and Guzzle accept every encoding, including `br` |
| Pre-patch inode snapshot | `find -type f -printf '%i'` on the real `.so` | the symlink inode is a false anchor |

## Repository layout

```text
src/
  common.sh      logging, version parsing, classification matrix, state_get
  detect.sh      read-only audit: verdict, JSON report, before-state
  remediate.sh   backup, authenticity gate, dry-run, install, restart, inode check
  verify.sh      version, CVE reference, stale mappings, failed units, round-trip
docs/
  THREAT-MODEL.md  decode-path exposure model and attack surface
  PLAYBOOK.md      phase-by-phase procedure for a human auditor
  FIELD-REPORT.md  anonymised results from three production hosts
wiki/             operator documentation, field cases, advisory digest
SKILL.md          agent skill: the minimal orchestration playbook
AGENTS.md         repository rules and invariants
```

## Requirements

- Target host: Debian 12, Ubuntu 24.04 or Ubuntu 26.04 with `apt`, root over an SSH key.
- `bash` 5.x, `dpkg`, `apt-get`, `systemd`; `curl` and `brotli` are optional and only widen the
  functional round-trip.
- No network access from the tooling beyond the distribution package mirrors.
- Output state: `/var/lib/cve-2026-111497/` (`report.json`, `state.before`, `state.after`).
- Backups: `/root/cve-2026-111497-fix-<timestamp>/` with old `.deb`, current `.so` and a `dpkg`
  snapshot.

## Limitations

- **apt only.** rpm-based hosts, Alpine and container base images are out of scope; the
  classification matrix is portable but the remediation path is not.
- **No static-linking detection.** A Brotli copy compiled into a binary or a container image is
  not visible to `dpkg`. The detector reports runtime versions it can read and nothing more.
- **No orchestration layer.** Envoy, Istio, .NET, Bun and Deno are described in the threat model
  but are not remediated by the scripts.
- **Desktop and infrastructure consumers are deferred, not restarted.** Font and GUI stacks map
  `libbrotlidec` through freetype; restarting them would drop the session. They are reported as
  `deferred` and reload the patched library on the next login or reboot.
- **`needrestart` may act first.** On Ubuntu 24.04 the package hook restarts server services
  during installation, so the restart section may legitimately find nothing left to do.
- **Field data is three hosts.** The report is a sample, not a fleet census.

## Documentation

| Document | Content |
|---|---|
| [`docs/THREAT-MODEL.md`](docs/THREAT-MODEL.md) | exposure model, decode-path inventory, severity reasoning |
| [`docs/PLAYBOOK.md`](docs/PLAYBOOK.md) | the procedure the scripts implement, for a human auditor |
| [`docs/FIELD-REPORT.md`](docs/FIELD-REPORT.md) | three hosts, nine defects found in the field, verification evidence |
| [Wiki](https://github.com/paulmann/CVE-2026-111497/wiki) | field cases with root causes, advisory digest, operating runbook |

## Languages

English is canonical: a document without a language suffix is the source of truth. Every
translation carries the suffix of its language and links back to the canonical file.

| Language | Files |
|---|---|
| English (canonical) | `README.md`, `AGENTS.md`, `SKILL.md`, `docs/*.md` |
| Russian | `*.ru.md` |
| German | `*.de.md` |
| Spanish | `*.es.md` |
| Chinese | `*.zh.md` |
| Hindi | `*.hi.md` |

The changelog and the wiki are English-only by policy: a release log and an operator handbook are
maintained as single sources, and a translation that lags behind is worse than none.

## Contributing

Fixes and new distro backports are welcome. Read [`CONTRIBUTING.md`](CONTRIBUTING.md) first: the
exit-code contract, the backup-before-change rule and the authenticity gate are not negotiable.
Security reports follow [`SECURITY.md`](SECURITY.md).

## License

MIT. The tool is defensive; it contains no credentials, no infrastructure identifiers and no
target addresses.
