From 7b7fcde84aa07f7b0ac3b374abdf3cf80dc261b6 Mon Sep 17 00:00:00 2001 From: veg Date: Sun, 31 May 2026 01:51:17 +0000 Subject: [PATCH] Close root-side TOCTOU in the export handoff 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. --- mm3-hk-export | 66 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/mm3-hk-export b/mm3-hk-export index 22f4d8f..f40f23e 100755 --- a/mm3-hk-export +++ b/mm3-hk-export @@ -134,31 +134,51 @@ case "$status" in ;; FILE) path=$(printf '%s' "$line" | cut -d'|' -f4) - # The web user created $path (mkstemp, 0600) in /var/tmp. Refuse to - # publish anything that isn't a plain regular file — guards against a - # symlink being swapped in before root reads it. - if [ -L "$path" ] || [ ! -f "$path" ]; then - echo "mm3-hk-export: refusing to publish non-regular export file: $path" >&2 - rm -f "$path" + mkdir -p "$(dirname "$out")" + # Publish the web-user temp file as root without re-resolving its path. + # The export lives in /var/tmp owned by the web user, so any process + # with that uid could swap the name for a symlink between a name-based + # check and a name-based copy (TOCTOU) and make root copy an arbitrary + # root-readable file. So: open the source ONCE with O_NOFOLLOW (a swapped + # symlink makes the open fail outright), confirm it's a regular file via + # the open fd, copy from that fd into a fresh root-owned temp in the + # destination dir, fsync, and atomically rename into place. Paths are + # passed as argv (heredoc is single-quoted) so nothing is interpolated. + if ! python3 - "$path" "$out" <<'PY' +import os, sys, stat, shutil, tempfile +src, out = sys.argv[1], sys.argv[2] +try: + fd = os.open(src, os.O_RDONLY | os.O_NOFOLLOW) +except OSError as e: + sys.exit("mm3-hk-export: cannot open export temp (symlink swapped in?): %s" % e) +dtmp = None +try: + if not stat.S_ISREG(os.fstat(fd).st_mode): + sys.exit("mm3-hk-export: export temp is not a regular file") + dfd, dtmp = tempfile.mkstemp(prefix=".mm3hk.", dir=os.path.dirname(out) or ".") + r = os.fdopen(fd, "rb"); fd = -1 # fd now owned by r + with r, os.fdopen(dfd, "wb") as w: # dfd now owned by w + shutil.copyfileobj(r, w) + w.flush() + os.fsync(w.fileno()) + os.replace(dtmp, out) # atomic same-dir rename, root-owned, 0600 + dtmp = None # consumed by the rename +except BaseException: + if dtmp is not None: + try: os.unlink(dtmp) + except OSError: pass + raise +finally: + if fd != -1: + try: os.close(fd) + except OSError: pass + try: os.unlink(src) + except OSError: pass +PY + then + echo "mm3-hk-export: failed to publish export to $out" >&2 exit 1 fi - dest_dir=$(dirname "$out") - mkdir -p "$dest_dir" - # Copy into a root-owned temp file *on the destination filesystem* so the - # final publish is an atomic same-fs rename (not a cross-fs copy+unlink - # that could leave a partial $out or clobber a good backup on failure), - # and so the artifact ends up owned by root, not the web user. - tmp=$(mktemp "$dest_dir/.mm3hk.XXXXXX") || { - echo "mm3-hk-export: could not create temp file in $dest_dir" >&2 - rm -f "$path"; exit 1 - } - if ! cp -- "$path" "$tmp"; then - echo "mm3-hk-export: failed to copy export into $dest_dir" >&2 - rm -f "$tmp" "$path"; exit 1 - fi - rm -f "$path" - chmod 600 "$tmp" - mv -- "$tmp" "$out" echo "mm3-hk-export: wrote $count messages to $out" >&2 ;; *)