From ddb3bc690880934f7d9ea7fff3171eb283f80c45 Mon Sep 17 00:00:00 2001 From: veg Date: Sun, 31 May 2026 05:46:07 +0000 Subject: [PATCH] Verify export integrity with SHA-256 before publishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- README.md | 10 ++++++++++ mm3-hk-export | 43 ++++++++++++++++++++++++++++++++----------- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e849b44..b723f91 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/mm3-hk-export b/mm3-hk-export index c1f2721..f2ae452 100755 --- a/mm3-hk-export +++ b/mm3-hk-export @@ -73,7 +73,7 @@ EOF } result=$(mailman-web shell <