# Playbook — CVE-2026-111497 remediation

The procedure the scripts implement, written so a human auditor can reproduce, audit or override
every step. Each phase names its evidence: the command whose output justifies the decision.

## Phase 0 — Perimeter inventory

Decide which hosts are in scope and confirm root access over a key.

```bash
for h in <host-alias-1> <host-alias-2>; do
  ssh "$h" 'grep PRETTY_NAME /etc/os-release; dpkg -l libbrotli1 2>/dev/null | tail -1'
done
```

Acceptance: every host answers with an OS name and either a package line or nothing.

## Phase 1 — Detection (read-only)

`detect.sh` collects, without changing the system:

1. Host identity and OS.
2. `libbrotli1` version and its classification in the vulnerable release matrix.
3. Whether a distribution backport is already applied (changelog names the CVE).
4. The apt candidate from `-security` after `apt-get update`.
5. Live consumers of `libbrotlidec` from `/proc/*/maps`.
6. Decode surface: libcurl, Node, Python, PHP, `ngx_brotli`, `mod_brotli`.
7. An inode snapshot of the real `.so` files, for the later stale-mapping comparison.

Verdict: `SAFE` (0), `VULNERABLE` (1), `NOT_INSTALLED` (3), `ERROR` (10).

Acceptance: `state.before` exists and contains `installed`, `candidate` and the three inodes.

## Phase 2 — Remediation

`remediate.sh` runs a strict sequence and stops at the first failed invariant:

1. **Backup** — old `.deb`, current `.so`, `dpkg -l` snapshot into
   `/root/cve-2026-111497-fix-<timestamp>/`.
2. **Authenticity gate** — download the candidate and require `CVE-2026-111497` in its packaged
   changelog. A candidate that only looks newer is rejected.
3. **Dry run** — `apt-get install --only-upgrade --dry-run`; the expected set is exactly the
   Brotli packages and nothing else.
4. **Installation** — `libbrotli1`, plus `libbrotli-dev` when it was installed before.
5. **Consumer restart** — every systemd unit whose processes map `libbrotlidec`, resolved to the
   innermost unit under `/system.slice/` so template instances are caught; panel daemons started
   as transient scopes are restarted through their own control tool.
6. **Inode check** — no server consumer may still map a pre-patch inode.

Acceptance: exit 0 and a `REMEDIATED:` line naming the old and new versions.

## Phase 3 — Verification

`verify.sh` confirms five facts:

1. The package version changed.
2. The installed changelog references `CVE-2026-111497`.
3. No server process maps a pre-patch inode.
4. `systemctl --failed` is empty.
5. A functional decode round-trip succeeds (local `br` endpoint or the `brotli` CLI).

Acceptance: `VERIFY: PASS`, exit 0.

## Manual cross-check (independent of the scripts)

```bash
dpkg -l | grep -E 'brotli'
apt-cache policy libbrotli1
zgrep -i CVE-2026-111497 /usr/share/doc/libbrotli1/changelog.Debian.gz
# no process may map the pre-patch decoder:
grep -l libbrotlidec /proc/[0-9]*/maps 2>/dev/null | wc -l
```

## Rollback

```bash
dpkg -i /root/cve-2026-111497-fix-<timestamp>/deb/libbrotli1_*.deb
# then restart every service listed in phase 2, step 5
```

Rollback restores the vulnerable version by definition. Use it only when the patched package
breaks a dependency, and record the decision.

## Temporary mitigation when no backport exists

Reject requests carrying `Content-Encoding: br` (including composite values such as `gzip, br`)
at the reverse proxy or WAF before they reach the application, and answer `415 Unsupported Media
Type`. This is a stopgap, not a substitute for the update: it protects inbound bodies only and
leaves every outbound libcurl fetch exposed.

## Closure criteria

- `detect.sh` returns 0 on a re-run, or `verify.sh` returns `VERIFY: PASS` after remediation.
- The backup directory is present and non-empty.
- Hosted sites answer with their pre-change status.
- The deferred desktop consumers are recorded in the report, with the expectation that they
  reload the patched library on the next login or reboot.
