[cleaner] refuse out-of-tree tar members on extract - #4461
Conversation
919dc98 to
99d2698
Compare
|
Congratulations! One of the builds has completed. 🍾 You can install the built RPMs by following these steps:
Please note that the RPMs should be used only in a testing environment. |
| data_filter = getattr(tarfile, 'data_filter', None) | ||
| if data_filter is not None: | ||
| try: | ||
| return data_filter(member, dest_path) |
There was a problem hiding this comment.
This sadly causes a regression on Python 3.12+ / whenever data_filter is present in tarfile class.
data_filter silently drops block devices and FIFOs, where esp. the block devices are legitimate members of a sosreport. While current sos clean keeps them in the archive.
Older Python versions would keep block devices and FIFOs, hence we would have an inconsistent behaviour of sos clean, depending on Python version used.
There is a legitimate question "oh, sos clean does not touch block device files, is that safe? Can't they contain some sensitive data to clean? Or.. isn't it safer to remove them..?" I am a noob on block devices, so I have no opinion how sos clean should handle them.
There was a problem hiding this comment.
I think one potential fix of this comment is modifying the except tarfile.FilterError to something like:
except tarfile.FilterError as e:
# data_filter rejects special files, but sosreports legitimately
# contain them (collected from /dev, /proc, etc.). Re-validate
# just the path/symlink safety and allow special files through.
if member.isdev() or member.isfifo():
# Still validate the path is within destination
dest_path_abs = os.path.abspath(dest_path)
member_path = os.path.abspath(os.path.join(dest_path_abs, member.name))
if _path_is_within(dest_path_abs, member_path):
return member # Safe special file, allow it
# All other FilterError cases: reject
return None
but that means some code duplication I dont like much..
There was a problem hiding this comment.
So a "block/char device" in this context is purely a device special file inode: it just carries inode information (mode/ownership/maj/min/xattrs). Similarly for named FIFOs. They have historically been included (along with much of /dev) in the tarball because they carry potentially useful diagnostic information.
There was a problem hiding this comment.
so I guess worth collecting them if they sit inside the archive directory, and ignore them if they are a target of a symlink outside archive.
| data_filter = getattr(tarfile, 'data_filter', None) | ||
| if data_filter is not None: |
There was a problem hiding this comment.
This code (as well as dest_path = os.path.abspath(dest_path)) is redundantly re-evaluated for each and every member of the archive (typical sosreport contains a few tens of thousands of files). Please make these calls just once.
Potential implementation here: can't you nest the rest of the method inside a nested function, and have:
def _make_safer_extract_filter(dest_path):
abs_dest = os.path.abspath(dest_path)
data_filter = getattr(tarfile, 'data_filter', None)
def filter_func(member, dest_path):
# here is vast majority of the original method
return filter_func
? (I dont insist on this specific idea, though)
| directory = os.path.abspath(directory) | ||
| target = os.path.abspath(target) |
There was a problem hiding this comment.
These os.path.abspath calls are redundant - the method is called for arguments already "abspath-ed".
|
I have nothing more to add to @pmoravec's initial review |
99d2698 to
8824f4a
Compare
bmr-cymru
left a comment
There was a problem hiding this comment.
Aside from @pmoravec 's points, I wonder if for the cleaner use case we couldn't get away with using the predefined 'tar' filter?
-
Strip leading slashes (/ and os.sep) from filenames.
-
Refuse to extract files with absolute paths (in case the name is absolute even after stripping slashes, e.g. C:/foo on Windows). This raises AbsolutePathError.
-
Normalize filenames (TarInfo.name) that contain .. components using os.path.normpath(). Note that this removes internal .. components, which may change the meaning of the name if it traverses symbolic links.
-
Refuse to extract files whose absolute path (after following symlinks) would end up outside the destination. This raises OutsideDestinationError.
-
Clear high mode bits (setuid, setgid, sticky) and group/other write bits (S_IWGRP | S_IWOTH).
The problematic parts here seem to be the name normalisation (which would alter the symlink structure), and the high mode bits. Could this be acceptable for the clean use cases?
| @@ -25,28 +25,73 @@ | |||
| # python older than 3.8 will hit a pickling error when we go to spawn a new | |||
| # process for extraction if this method is a part of the SoSObfuscationArchive | |||
| # class. So, the simplest solution is to remove it from the class. | |||
There was a problem hiding this comment.
This comment has become detached from the code it originally described
| prefix = os.path.commonprefix([abs_directory, abs_target]) | ||
| if prefix != abs_directory: | ||
| raise Exception(f"Attempted path traversal in tarfle" | ||
| f"{prefix} != {abs_directory}") |
There was a problem hiding this comment.
This is the source of the current weakness; PEP706 rightly says: "When verifying members in advance, it may be necessary to track how each member would have changed the filesystem, e.g. how symlinks are being set up. This is hard. We can’t expect users to do it." (emph added).
|
Preliminary review reveals no issue. We might consider adding some logging when untar-ing rejects a file, not sure how worth it is. I will review the PR further more today. |
pmoravec
left a comment
There was a problem hiding this comment.
ACK from me. Unsure if it is worth to shout out (even as info/debug log) we are dropping extraction of some files outside the directory.
Hard to compare the PR to Bryn's proposal. It sounds to me similarly good as this one, per the description. Can it work for all supported Python versions? Will we get shorter or cleaner code?
Putting that discussion aside, for the matter of fixing the vulnerability, the PR sounds good to me.
|
@pmoravec agreed: my only outstanding request for this PR is to fix the comment position above the With a fix & a regression test in place we can revisit this in future and possibly simplify things further. |
| self.assertTrue(found_link and os.path.islink(found_link)) | ||
| self.assertEqual(os.readlink(found_link), 'motd') | ||
| os.remove(tar_path) | ||
|
|
There was a problem hiding this comment.
pylint complains:
tests/unittests/cleaner_tests.py:788:0: C0305: Trailing newlines (trailing-newlines)
| handle = tempfile.NamedTemporaryFile(suffix='.tar', delete=False) | ||
| handle.close() |
There was a problem hiding this comment.
pylint complains:
tests/unittests/cleaner_tests.py:735:17: R1732: Consider using 'with' for resource-allocating operations (consider-using-with)
sos clean used fully_trusted_filter plus a member-name-only path guard. That does not stop a symlink whose target is an absolute host path followed by a regular file with the same relative name, so extraction can write outside the dest directory. Use PEP-706 data_filter when available and skip FilterError members. Keep in-tree relative symlinks. Assisted-by: Cursor <https://cursor.com> Signed-off-by: Sandipan Roy <saroy@redhat.com>
8824f4a to
850dcbd
Compare
pmoravec
left a comment
There was a problem hiding this comment.
LGTM.
Please @TurboTurtle or @arif-ali or @bmr-cymru for 2nd review, to let it merged.
|
Looks like test is failing on Debian 12: |
Python 3.11.2 (Debian 12) ignores TarFile.extraction_filter, so the absolute-symlink TarSlip still wrote outside dest. Drop rejected members before extractall; keep the PEP-706 filter on 3.12+. Assisted-by: Cursor <https://cursor.com> Signed-off-by: Sandipan Roy <saroy@redhat.com>
Closes #4460
sos cleanusedfully_trusted_filterplus a member-name-only path guard. That does not stop a symlink whose target is an absolute host path followed by a regular file with the same relative name, so extraction can write outside the dest directory.Use PEP-706
data_filterwhen available and skipFilterErrormembers. Keep in-tree relative symlinks.Please place an 'X' inside each '[]' to confirm you adhere to our Contributor Guidelines