Bug 16241 - safe_tarfile: the 'tar' extraction filter does not bound link targets, so the class is less strict on new Python than on old
Summary: safe_tarfile: the 'tar' extraction filter does not bound link targets, so the...
Status: NEW
Alias: None
Product: Samba 4.1 and newer
Classification: Unclassified
Component: Python (show other bugs)
Version: unspecified
Hardware: All All
: P5 normal (vote)
Target Milestone: ---
Assignee: Douglas Bagnall
QA Contact: Samba QA Contact
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2026-09-10 20:43 UTC by Shakir Mahmood
Modified: 2026-09-10 20:45 UTC (History)
0 users

See Also:


Attachments
patch: bound link targets in safe_tarfile ('data' filter for link members only), plus three tests (7.22 KB, patch)
2026-09-10 20:44 UTC, Shakir Mahmood
shaker.zxc77: review?
shaker.zxc77: ci-passed-
Details
reproducer: probe-samba-fix.py (runs master's behaviour and the patched behaviour side by side, with controls) (3.30 KB, text/plain)
2026-09-10 20:45 UTC, Shakir Mahmood
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description Shakir Mahmood 2026-09-10 20:43:28 UTC
Filed at the invitation of Andrew Bartlett, in reply to a hardening note I
sent privately to security@samba.org on 2026-08-28. Tested against current
master (python/samba/safe_tarfile.py as of 2026-09-09); the version field
offers no "master", hence "unspecified".

python/samba/safe_tarfile.py selects tarfile's intermediate 'tar' extraction
filter, with the comment:

    # The 'data' filter preserves no permissions so we select the
    # intermediate 'tar' filter here which prevents escape but
    # preserves permissions.

The 'tar' filter bounds member PATHS. It does not bound link TARGETS.

In CPython's tarfile._get_filtered_attrs(), the two checks that reject a link
member whose target leaves the destination -- AbsoluteLinkError and
LinkOutsideDestinationError -- are gated behind for_data=True. They therefore
run under the 'data' filter only. Under 'tar', a symlink member whose linkname
is an absolute path, or a relative path climbing out of the destination, is
extracted as given.

The point of raising it here is not the archive format. It is that the
pre-PEP-706 fallback in this same class already refuses those members:

    _is_unsafe_symlink()  rejects a symlink whose resolved target leaves the dir
    _is_unsafe_link()     rejects a hardlink whose resolved target leaves the dir

So on a Python without extraction filters, safe_tarfile refuses these members;
on a current Python it accepts them. The class became less strict as the
interpreter improved, which I take to be the opposite of what the try/except
was written to do.

MEASURED, Python 3.10.12, with controls so that a "blocked" result cannot come
from a broken harness. Reproducer attached (probe-samba-fix.py); it builds each
archive in a temp directory and reports what actually lands on disk.

  payload                        master                    with patch
  ---------------------------------------------------------------------------
  symlink -> ../../../escape     created, points outside   LinkOutsideDestinationError
  symlink -> /etc/passwd         created, points outside   AbsoluteLinkError
  hardlink -> ../../../escape/x  KeyError                  LinkOutsideDestinationError
  CONTROL benign file            extracted                 extracted
  CONTROL '../' member path      OutsideDestinationError   OutsideDestinationError
  CONTROL symlink -> ok.txt      created, inside           created, inside

Two things the table is meant to settle. First, the 'tar' filter does close the
original CVE-2007-4559 path-traversal case -- the '../' member control is
refused both before and after -- so this is only the link-target half. Second, a
legitimate symlink that stays inside the destination is still extracted after
the change, so the fix does not cost you working behaviour.

For completeness, and so this is not overstated: I also tested writing THROUGH
such a symlink with a later member in the same archive. CPython raises
OutsideDestinationError for that. This is file-read and link-placement, not
arbitrary write.

WHY THIS IS NOT FILED AS A SECURITY ISSUE

The consumers are python/samba/ntacls.py and
python/samba/netcmd/domain/backup.py, i.e. the "samba-tool domain backup
restore" path. Restoring a domain backup is already a privileged operation on a
file the administrator has chosen to trust, and that file reconstitutes an
entire directory service. The harm case needs the victim to opt in, so I do not
think it clears the bar for an advisory, and I said so when I first raised it
privately. Andrew Bartlett agreed and asked me to file it publicly. Treat it as
code hardening plus a comment-accuracy issue and score it however you see fit;
I will not argue the severity.

SUGGESTED FIX (patch attached)

Apply the 'data' filter to link members only, and keep 'tar' for everything
else, so ordinary members keep the permissions that motivated choosing 'tar':

    _tar_filter = staticmethod(tarfile.tar_filter)
    _data_filter = staticmethod(tarfile.data_filter)

    def extraction_filter(self, member, path):
        if member.issym() or member.islnk():
            return self._data_filter(member, path)
        return self._tar_filter(member, path)

The attached patch does that, updates the comment above it (that comment is the
sentence a future reader will rely on), and adds three tests to
samba.tests.safe_tarfile, which selftest/tests.py already plans. The two escape
tests fail on master and pass with the change; the in-destination control and
the two existing tests pass either way. The patch carries a Signed-off-by line
per the copyright policy, and a BUG: line I will fill in with this bug's number.

I have not built it against a full Samba tree -- I have no Samba build here --
so the module was exercised standalone with a stand-in for TestCaseInTempDir.
Your CI is the real check, and I would rather say that than imply otherwise.

A SECOND, MINOR OBSERVATION

In the same fallback branch, _is_traversal_attempt() tests
tarinfo.name.startswith(os.sep) and ".." + os.sep. Tar member names use '/' by
specification, so on a Windows host os.sep is the backslash and neither test
fires. Samba does not run its Python tooling on Windows as far as I know, so I
mention it only for completeness and it is not addressed in the patch.

RELATED

The underlying documentation gap is python/cpython#156026: nothing in the
tarfile docs states plainly that the 'tar' filter does not bound link targets.
Several projects besides Samba have read it the same way.

--
Shakir Mahmood Shakir
Independent security researcher
GitHub: drengir1
Comment 1 Shakir Mahmood 2026-09-10 20:44:42 UTC
Created attachment 19200 [details]
patch: bound link targets in safe_tarfile ('data' filter for link members only), plus three tests

Patch against master, Signed-off-by present per the copyright policy, BUG: line pointing at this bug.

It keeps the 'tar' filter for ordinary members, so permissions are preserved, and applies the 'data' filter to link members only, which is the only stdlib filter that bounds link targets. The comment above the filter selection is updated too, since that sentence is what a future reader will rely on.

Three tests are added to samba.tests.safe_tarfile, which selftest/tests.py already plans. Both escape tests fail on master and pass with the change; the third is a control asserting that a legitimate symlink pointing inside the destination is still extracted, so the fix does not cost working behaviour.

I have marked the CI flag '-' deliberately: I have no Samba build on this machine, so this exact patch has NOT been through GitLab CI or autobuild. The module was exercised standalone on Python 3.10.12 with a stand-in for TestCaseInTempDir. Please treat your CI as the real check.

Happy to rework it in any direction you prefer, including the narrower option of only correcting the comment.
Comment 2 Shakir Mahmood 2026-09-10 20:45:55 UTC
Created attachment 19201 [details]
reproducer: probe-samba-fix.py (runs master's behaviour and the patched behaviour side by side, with controls)

Standalone, no dependencies, no network. It copies the current class body from python/samba/safe_tarfile.py and the patched variant, builds each archive in a temporary directory, extracts, and reports what actually lands on disk. Run it with any Python that has the PEP 706 filters:

    python3 probe-samba-fix.py

The controls are the point. A benign file must extract, a '../' member path must be refused, and a legitimate in-destination symlink must still be created, so a 'blocked' verdict cannot come from a broken harness or from a host that cannot create symlinks. Output on Python 3.10.12 is the table in comment 0.