The prior handoff checked the /var/tmp
temp file by name (-L/-f) and then reopened it by name with cp: a
process holding the web uid could swap the path for a symlink in that
window and make root copy an arbitrary root-readable file into $out.
Replace the check-then-reopen with a single root-side python3 step that
opens the source ONCE with O_NOFOLLOW (a swapped symlink makes the open
fail), verifies it is a regular file via the open fd, copies from that
fd into a fresh root-owned mkstemp temp in the destination dir, fsyncs,
and atomically os.replace()s it into place. The path is never
re-resolved by name, so the TOCTOU is closed rather than narrowed.
Paths are passed as argv (heredoc single-quoted), so nothing is
interpolated into the snippet. Verified: regular file publishes 0600
with src removed and no leftover temp; symlink source is rejected
(ELOOP) with dest untouched; existing backups are atomically replaced.
Address three hardening findings:
- Gate the README backup-and-destroy loop so `mailman-wrapper remove`
only runs after both the export and member dump succeed; a failed
backup now leaves the list intact instead of falling through to an
irreversible delete. Empty/no-archive lists still pass (exit 0 by
design), so only real failures block removal.
- Replace the curl/REST archive_policy lookup with `mailman-wrapper
shell`, reading archive_policy straight from core's DB via
getUtility(IListManager). Eliminates REST credential handling
entirely (no secret in argv / /proc), drops the curl dependency, and
removes the @->. list-id conversion.
- Make the final publish a safe privilege handoff: reject non-regular /
symlinked temp files, copy the web-user mkstemp output into a
root-owned temp file on the destination filesystem, chmod 600, then
atomically rename into place. Fixes inherited web-user ownership and
the cross-filesystem mv that could leave a partial or clobbered
backup.
Scriptable equivalent of HyperKitty's web Download button for
backup-and-destroy loops. Runs the export through Debian's mailman-web
wrapper (HyperKitty lives in the web DB, separate from core), reproducing
the web view's Email.as_bytes() output.
- Handles the three archive states without writing partial/empty files:
populated (writes mbox), row-but-empty, and no row at all.
- For a missing row, best-effort core REST lookup distinguishes
archive_policy=never from 'nothing archived yet'.
- -z/--gzip matches the web .mbox.gz (deterministic header, mtime=0).
- Privilege-safe: web user writes a 0600 mkstemp file, root moves it.
- Address validated before interpolation into the embedded Python.