Verify export integrity with SHA-256 before publishing

The O_NOFOLLOW + S_ISREG handoff blocks a
symlink swap but still accepts a *regular-file* swap — a process holding
the web uid can replace/truncate the /var/tmp temp with different content
before root copies it, and root would publish it and exit 0, which the
destroy loop reads as authorization to remove the list.

The exporter now hashes the bytes it wrote (read back through its own
held fd — the original inode, immune to any name swap) and returns the
SHA-256 on the FILE status line. Root copies the source while hashing and
refuses to publish unless the digest matches; a swap/truncation/partial
write yields a mismatch, a non-zero exit, and no removal.

This does NOT defend a fully compromised mailman-web process (it controls
the source DB the archive is built from) — that is inherent and now
documented as out of scope. It closes same-uid swaps, partial writes, and
accidental corruption while staying in the existing architecture.

Verified: held-fd hash equals an independent by-name read (gzip + plain);
correct digest publishes (0600, content intact, source unlinked); wrong
digest, post-hash swap, and empty digest are all rejected with nothing
published; end-to-end NONE/EMPTY still exit 3, bad digest exits 1.
This commit is contained in:
veg 2026-05-31 05:46:07 +00:00
parent 7c9b13051d
commit ddb3bc6908
2 changed files with 42 additions and 11 deletions

View file

@ -111,3 +111,13 @@ a failed export must still halt the destroy step.)
incrementally, avoiding the web export's known blow-up on large lists.
- **Re-import.** The output is valid mbox (proper `From ` separators,
`From`-mangled bodies) and can be re-imported with `hyperkitty_import`.
- **Handoff integrity.** The archive is written by the privilege-dropped web
user into `/var/tmp`, then published by root. Root opens that file with
`O_NOFOLLOW`, copies it while computing a SHA-256, and refuses to publish
unless the digest matches the one the exporter computed over the bytes it
wrote, so a same-uid process that swaps, truncates, or corrupts the temp
file between export and publish is caught (the run fails, nothing is
published, and the destroy loop will not remove the list). A fully
compromised mailman-web process is out of scope: it builds the archive from
a database it already controls, so no file-level check downstream can vouch
for the content it produced.