# Threat model — CVE-2026-111497 (Brotli decoder out-of-bounds write)

This document defines what the tool considers exposed, what it explicitly does not cover, and why
the classification is drawn where it is. It is the reference for interpreting every verdict the
scripts return.

## 1. The vulnerability

| Property | Value |
|---|---|
| Identifier | CVE-2026-111497 |
| Component | Google Brotli, decoder (`c/dec/decode.c`) |
| Weakness | CWE-787, out-of-bounds write |
| Vulnerable upstream releases | 1.0.3 … 1.0.9, 1.1.0, 1.2.0 |
| Fixed upstream releases | 1.0.10, 1.1.1, 1.2.1 (21 October 2026) |
| Attack vector | crafted Brotli stream handed to the decoder |
| Authentication required | no |
| Impact | process crash, memory corruption, in some configurations remote code execution |
| Severity | CVSS 4.0 9.2 as published by the project; 8.5 on the CVSS 3.x scale in distribution trackers |

The defective path is the decoder state machine at a block transition: a malformed stream drives
the internal counters past the intended boundary, and the decoder writes beyond the allocated
output buffer instead of reporting an invalid stream early. The upstream fix detects and reports
the invalid stream before the write.

## 2. What is exposed and what is not

The distinction that decides most verdicts:

- An **encoder** (nginx `ngx_brotli`, `mod_brotli`, static asset pipelines) compresses data the
  server itself produced. It never parses attacker-controlled Brotli. It is not the attack
  surface, even though it links the same package.
- A **decoder** accepts data from outside and parses it. That is the attack surface.

| Path | Exposed when | Notes |
|---|---|---|
| HTTP request body | the application or a proxy decompresses `Content-Encoding: br` | the highest-impact vector where it exists |
| Outbound HTTP client | libcurl with `CURLOPT_ENCODING` empty, Node `fetch`, Python `httpx`/`requests` with Brotli | a malicious or compromised third party answers with `br` |
| Response chain through a proxy | Envoy/Istio HTTP decompressor enabled | the proxy becomes the decoder |
| File upload or import | the application decompresses Brotli archives or streams | often overlooked in audits |
| Bundled runtime | Node.js, Bun, .NET request decompression | the fix arrives with the runtime, not with `libbrotli1` |

## 3. Why libcurl dominates real fleets

Measured on three production hosts: the reachable vector was never "requests to the web server",
because nginx and Apache used Brotli only for compression and neither decoded request bodies. The
dominant path was **outbound HTTP through libcurl**, reached from PHP application code.

`CURLOPT_ENCODING` set to an empty string tells libcurl to advertise and accept every encoding it
supports, including `br`. That single argument appears in:

- WordPress `wp-includes/Requests/src/Transport/Curl.php`;
- WordPress SimplePie `File.php`;
- Guzzle handler factories shipped inside plugins;
- direct `wp_remote_*` traffic, RSS fetches and vendor API calls.

On one host the inventory counted 164 PHP files carrying `CURLOPT_ENCODING` or
`CURLOPT_ACCEPT_ENCODING`. That number is the real measure of exposure on a shared hosting box —
not the presence of the package alone.

## 4. Attacker model

| Attacker position | Capability | Consequence |
|---|---|---|
| Remote, unauthenticated | sends a crafted Brotli body or serves a crafted `br` response | crash, memory corruption, possible code execution in the decoding process |
| Third-party content provider | controls an endpoint the server fetches | reaches the libcurl decode path without touching the perimeter |
| Local unprivileged | supplies a file the application decompresses | same decoder, lower impact |

The tool assumes the attacker can choose the bytes and cannot choose the process: the goal of the
audit is to make sure no reachable decoder remains in a vulnerable process.

## 5. Detection strategy

Presence of a package is not exposure. The detector therefore records four independent facts and
classifies only after all four are known:

1. the installed version, with epoch and distribution revision stripped to the upstream number;
2. whether the packaged changelog already names the CVE (distribution backport applied);
3. whether the apt candidate is newer than the installed version (a supported fix exists);
4. which running processes map `libbrotlidec`, and through which runtime.

A host is `VULNERABLE` only when the upstream version is in the vulnerable range **and** no
backport is present. A host is `SAFE` when either condition fails — including the case of a
distribution revision that keeps the vulnerable-looking upstream number while carrying the fix.

## 6. Out of scope

| Area | Reason |
|---|---|
| Statically linked copies | invisible to `dpkg`; would need binary scanning per file |
| Container images | require registry-side scanning, not host-side inspection |
| Envoy, Istio, .NET, Bun, Deno | separate remediation lifecycles; documented, not automated |
| Windows and macOS hosts | the tool is bash and apt only |
| Application-level mitigations | blocking `Content-Encoding: br` is a stopgap the operator applies, not a fix |

## 7. Residual risk after remediation

1. Desktop and infrastructure processes keep the old library mapped until the next login or
   reboot. They are not a server decode path; the detector reports them as `deferred`.
2. A process started from a package installed **after** remediation but before the library was
   replaced would inherit the patched library — the ordering inside `remediate.sh` prevents the
   opposite case.
3. Application code that bundles its own Brotli (vendored source, static build) is unaffected by
   the package update and must be rebuilt separately.

See [PLAYBOOK.md](PLAYBOOK.md) for the procedure and [FIELD-REPORT.md](FIELD-REPORT.md) for what
these three facts looked like on real hosts.
