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.
This commit is contained in:
parent
c2b304e4a9
commit
7b7fcde84a
1 changed files with 43 additions and 23 deletions
|
|
@ -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
|
||||
;;
|
||||
*)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue