From 5de16ccc51e4bd9db598f9e859c706a38e678b1b Mon Sep 17 00:00:00 2001 From: Ali Raza Date: Fri, 12 Sep 2025 15:01:00 +0500 Subject: [PATCH 1/2] fix CWE-23: prevent zipslip/directory traversal attacks Add filter='data' parameter to all tar.extractall() calls to prevent directory traversal (zipslip) vulnerabilities. This change ensures that tar extraction operations reject any archive members that attempt to write outside the intended extraction directory. Affected modules: - amg: restore operations from Grafana archive files - aosm: helm package extraction for AOSM definitions - confcom: container image tar file processing and manifest extraction - connectedk8s: Arc Connectivity proxy binary extraction - containerapp: Java buildpack source code extraction - networkcloud: custom action result blob extraction - ssh: SSH proxy binary extraction from MCR packages The filter='data' parameter is available in Python 3.11.4+ and provides built-in protection against malicious tar archives containing entries with absolute paths or relative paths that traverse outside the extraction directory. References: - https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall - https://cwe.mitre.org/data/definitions/23.html --- src/amg/azext_amg/restore.py | 2 +- src/aosm/azext_aosm/common/utils.py | 4 ++-- src/confcom/azext_confcom/os_util.py | 8 ++++---- .../azext_confcom/tests/latest/test_confcom_fragment.py | 2 +- .../azext_confcom/tests/latest/test_confcom_tar.py | 2 +- .../azext_connectedk8s/clientproxyhelper/_binaryutils.py | 2 +- .../latest/test_containerapp_create_update_up_java.py | 2 +- .../azext_networkcloud/operations/custom_properties.py | 4 ++-- src/ssh/azext_ssh/connectivity_utils.py | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/amg/azext_amg/restore.py b/src/amg/azext_amg/restore.py index 5adc9dcd34c..d7ca37cc5b0 100644 --- a/src/amg/azext_amg/restore.py +++ b/src/amg/azext_amg/restore.py @@ -38,7 +38,7 @@ def restore(grafana_url, archive_file, components, http_headers, destination_dat with tarfile.open(name=archive_file, mode='r:gz') as tar: with tempfile.TemporaryDirectory() as tmpdir: - tar.extractall(tmpdir) + tar.extractall(tmpdir, filter='data') tar.close() _restore_components(grafana_url, restore_functions, tmpdir, components, http_headers, destination_datasources=destination_datasources) diff --git a/src/aosm/azext_aosm/common/utils.py b/src/aosm/azext_aosm/common/utils.py index 00cbb42feb5..ea6fc1f0bad 100644 --- a/src/aosm/azext_aosm/common/utils.py +++ b/src/aosm/azext_aosm/common/utils.py @@ -101,10 +101,10 @@ def extract_tarfile(file_path: Path, target_dir: Path) -> Path: if file_extension in (".gz", ".tgz"): with tarfile.open(file_path, "r:gz") as tar: - tar.extractall(path=target_dir) + tar.extractall(path=target_dir, filter='data') elif file_extension == ".tar": with tarfile.open(file_path, "r:") as tar: - tar.extractall(path=target_dir) + tar.extractall(path=target_dir, filter='data') else: raise InvalidFileTypeError( f"ERROR: The helm package, '{file_path}', is not" diff --git a/src/confcom/azext_confcom/os_util.py b/src/confcom/azext_confcom/os_util.py index 5a5e8deb638..8eb547a4f54 100644 --- a/src/confcom/azext_confcom/os_util.py +++ b/src/confcom/azext_confcom/os_util.py @@ -170,7 +170,7 @@ def map_image_from_tar_backwards_compatibility(image_name: str, tar: TarFile, ta # extract just the manifest file and see if any of the RepoTags match the image_name we're searching for # the manifest.json should have a list of all the image tags # and what json files they map to to get env vars, startup cmd, etc. - tar.extract("manifest.json", path=tar_dir) + tar.extract("manifest.json", path=tar_dir, filter='data') manifest_path = os.path.join(tar_dir, "manifest.json") manifest = load_json_from_file(manifest_path) # if we match a RepoTag to the image, stop searching @@ -187,7 +187,7 @@ def map_image_from_tar_backwards_compatibility(image_name: str, tar: TarFile, ta if not info_file: return None - tar.extract(info_file.name, path=tar_dir) + tar.extract(info_file.name, path=tar_dir, filter='data') # get the path of the json file and read it in image_info_file_path = os.path.join(tar_dir, info_file.name) @@ -259,7 +259,7 @@ def map_image_from_tar(image_name: str, tar: TarFile, tar_location: str): # extract just the manifest file and see if any of the RepoTags match the image_name we're searching for # the manifest.json should have a list of all the image tags # and what json files they map to to get env vars, startup cmd, etc. - tar.extract(info_file_name, path=tar_dir) + tar.extract(info_file_name, path=tar_dir, filter='data') manifest_path = os.path.join(tar_dir, info_file_name) manifest = load_json_from_file(manifest_path) try: @@ -274,7 +274,7 @@ def map_image_from_tar(image_name: str, tar: TarFile, tar_location: str): if not info_file: return None - tar.extract(info_file, path=tar_dir) + tar.extract(info_file, path=tar_dir, filter='data') # get the path of the json file and read it in image_info_file_path = os.path.join(tar_dir, info_file) diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_fragment.py b/src/confcom/azext_confcom/tests/latest/test_confcom_fragment.py index 2725ede31c0..d87ec0037c0 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_fragment.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_fragment.py @@ -498,7 +498,7 @@ def test_tar_file_fragment(self): tar_mapping_file = {"mcr.microsoft.com/aks/e2e/library-busybox:master.220314.1-linux-amd64": filename2} create_tar_file(filename) with TarFile(filename, "r") as tar: - tar.extractall(path=folder) + tar.extractall(path=folder, filter='data') with TarFile.open(filename2, mode="w") as out_tar: out_tar.add(os.path.join(folder, "index.json"), "index.json") diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_tar.py b/src/confcom/azext_confcom/tests/latest/test_confcom_tar.py index ab2733745f5..52abacffcb1 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_tar.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_tar.py @@ -180,7 +180,7 @@ def test_oci_tar_file(self): tar_mapping_file = {"mcr.microsoft.com/aks/e2e/library-busybox:master.220314.1-linux-amd64": filename2} create_tar_file(filename) with TarFile(filename, "r") as tar: - tar.extractall(path=folder) + tar.extractall(path=folder, filter='data') with TarFile.open(filename2, mode="w") as out_tar: out_tar.add(os.path.join(folder, "index.json"), "index.json") diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py index c655b4269de..e2687eab7fb 100644 --- a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py @@ -182,7 +182,7 @@ def _extract_proxy_tar_files( members.append(member) - tar.extractall(members=members, path=install_dir) + tar.extractall(members=members, path=install_dir, filter='data') def _check_proxy_installation( diff --git a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_create_update_up_java.py b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_create_update_up_java.py index d0369ac4a74..9178c1be788 100644 --- a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_create_update_up_java.py +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_create_update_up_java.py @@ -37,7 +37,7 @@ def download_java_source(source_path): shutil.rmtree(source_path) with tarfile.open(temp_file.name, 'r:gz') as tar: - tar.extractall(path=source_path) + tar.extractall(path=source_path, filter='data') os.remove(temp_file.name) diff --git a/src/networkcloud/azext_networkcloud/operations/custom_properties.py b/src/networkcloud/azext_networkcloud/operations/custom_properties.py index 3de4fb6237a..53f420c9e9e 100644 --- a/src/networkcloud/azext_networkcloud/operations/custom_properties.py +++ b/src/networkcloud/azext_networkcloud/operations/custom_properties.py @@ -59,7 +59,7 @@ def _output(parent_cmd, *args, **kwargs): # pylint: disable=unused-argument try: with urllib.request.urlopen(result_url) as result: with tarfile.open(fileobj=result, mode="r:gz") as tar: - tar.extractall(path=output_directory) + tar.extractall(path=output_directory, filter='data') logger.warning( "Extracted results are available in directory: %s", output_directory, @@ -126,7 +126,7 @@ def _output(parent_cmd, *args, **kwargs): # pylint: disable=unused-argument try: # Extract the downloaded blob with tarfile.open(downloaded_blob_name, mode="r:gz") as tar: - tar.extractall(path=output_directory) + tar.extractall(path=output_directory, filter='data') logger.warning( "Extracted results are available in directory: %s", output_directory, diff --git a/src/ssh/azext_ssh/connectivity_utils.py b/src/ssh/azext_ssh/connectivity_utils.py index cd7596d2da3..19ef02a8cdd 100644 --- a/src/ssh/azext_ssh/connectivity_utils.py +++ b/src/ssh/azext_ssh/connectivity_utils.py @@ -325,7 +325,7 @@ def _extract_proxy_tar_files(proxy_package_path, install_dir, proxy_name): members.append(member) - tar.extractall(members=members, path=install_dir) + tar.extractall(members=members, path=install_dir, filter='data') def _check_proxy_installation(install_dir, proxy_name): From 109ed4c0090683b6561985aa1429e4aecc32906a Mon Sep 17 00:00:00 2001 From: Ali Raza Date: Sat, 13 Sep 2025 02:38:44 +0500 Subject: [PATCH 2/2] chore: bump extension versions and update HISTORY for zipslip security fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update version numbers and HISTORY entries for extensions affected by the zipslip security fix in commit 5de16ccc51e4bd9db598f9e859c706a38e678b1b: - amg: 2.8.1 → 2.8.2 - aosm: 2.0.0b2 → 2.0.0b3 - confcom: 1.2.7 → 1.2.8 - connectedk8s: 1.10.8 → 1.10.9 - networkcloud: 3.0.0 → 3.0.1 - ssh: 2.0.6 → 2.1.0 Each extension's HISTORY file now documents the security fix that prevents zipslip/directory traversal attacks during tar archive extraction operations. Resolves maintainer feedback for release preparation. --- src/amg/HISTORY.rst | 4 ++++ src/amg/setup.py | 2 +- src/aosm/HISTORY.rst | 4 ++++ src/aosm/setup.py | 2 +- src/confcom/HISTORY.rst | 4 ++++ src/confcom/setup.py | 2 +- src/connectedk8s/HISTORY.rst | 4 ++++ src/connectedk8s/setup.py | 2 +- src/networkcloud/HISTORY.rst | 4 ++++ src/networkcloud/setup.py | 2 +- src/ssh/HISTORY.md | 4 ++++ src/ssh/setup.py | 2 +- 12 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/amg/HISTORY.rst b/src/amg/HISTORY.rst index 084c46a6688..6ca9bc8da55 100644 --- a/src/amg/HISTORY.rst +++ b/src/amg/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +2.8.2 +++++++ +* Security fix: prevent zipslip/directory traversal attacks during tar archive extraction in restore operations + 0.1.0 ++++++ * Initial release. diff --git a/src/amg/setup.py b/src/amg/setup.py index 74148fe0eac..b1228e49f56 100644 --- a/src/amg/setup.py +++ b/src/amg/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '2.8.1' +VERSION = '2.8.2' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers diff --git a/src/aosm/HISTORY.rst b/src/aosm/HISTORY.rst index 02a2950e053..20f9f322624 100644 --- a/src/aosm/HISTORY.rst +++ b/src/aosm/HISTORY.rst @@ -2,6 +2,10 @@ Release History =============== +2.0.0b3 +++++++++ +* Security fix: prevent zipslip/directory traversal attacks during tar archive extraction in helm package processing + 2.0.0b2 ++++++++ * Remove msrestazure dependency diff --git a/src/aosm/setup.py b/src/aosm/setup.py index 29b06442315..9b4946129b0 100644 --- a/src/aosm/setup.py +++ b/src/aosm/setup.py @@ -17,7 +17,7 @@ # Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = "2.0.0b2" +VERSION = "2.0.0b3" # The full list of classifiers is available at diff --git a/src/confcom/HISTORY.rst b/src/confcom/HISTORY.rst index 3006fd18e73..1bbc01c1954 100644 --- a/src/confcom/HISTORY.rst +++ b/src/confcom/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +1.2.8 +++++++ +* Security fix: prevent zipslip/directory traversal attacks during tar archive extraction in container image and manifest processing + 1.2.7 ++++++ * bugfix making it so that oras discover function doesn't error when no fragments are found in the remote repository diff --git a/src/confcom/setup.py b/src/confcom/setup.py index 3ce7907b25a..954d44b1874 100644 --- a/src/confcom/setup.py +++ b/src/confcom/setup.py @@ -19,7 +19,7 @@ logger.warn("Wheel is not available, disabling bdist_wheel hook") -VERSION = "1.2.7" +VERSION = "1.2.8" # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers diff --git a/src/connectedk8s/HISTORY.rst b/src/connectedk8s/HISTORY.rst index 28c23e6b0f7..ea4df5b6a9b 100644 --- a/src/connectedk8s/HISTORY.rst +++ b/src/connectedk8s/HISTORY.rst @@ -2,6 +2,10 @@ Release History =============== +1.10.9 +++++++ +* Security fix: prevent zipslip/directory traversal attacks during tar archive extraction in Arc Connectivity proxy binary processing + 1.10.8 ++++++ * Force delete parameter updated to `connectedk8s delete` command to allow force deletion of connectedk8s ARM resource. diff --git a/src/connectedk8s/setup.py b/src/connectedk8s/setup.py index 4f9959412ad..e33b3849ff7 100644 --- a/src/connectedk8s/setup.py +++ b/src/connectedk8s/setup.py @@ -13,7 +13,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = "1.10.8" +VERSION = "1.10.9" # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers diff --git a/src/networkcloud/HISTORY.rst b/src/networkcloud/HISTORY.rst index ba1d0435c4c..ad2fae6a673 100644 --- a/src/networkcloud/HISTORY.rst +++ b/src/networkcloud/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +3.0.1 +++++++++ +* Security fix: prevent zipslip/directory traversal attacks during tar archive extraction in custom action result blob processing + 3.0.0 ++++++++ * This stable version supports NetworkCloud 2025-02-01 APIs. diff --git a/src/networkcloud/setup.py b/src/networkcloud/setup.py index fb189c5db6b..717491cc205 100644 --- a/src/networkcloud/setup.py +++ b/src/networkcloud/setup.py @@ -10,7 +10,7 @@ # HISTORY.rst entry. -VERSION = '3.0.0' +VERSION = '3.0.1' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers diff --git a/src/ssh/HISTORY.md b/src/ssh/HISTORY.md index c681d5706c2..eefcc6d6cd8 100644 --- a/src/ssh/HISTORY.md +++ b/src/ssh/HISTORY.md @@ -1,5 +1,9 @@ Release History =============== +2.1.0 +----- +* Security fix: prevent zipslip/directory traversal attacks during tar archive extraction in SSH proxy binary processing + 2.0.6 ----- * Remove msrestazure dependency diff --git a/src/ssh/setup.py b/src/ssh/setup.py index 4ad30289af0..fe7d2e17745 100644 --- a/src/ssh/setup.py +++ b/src/ssh/setup.py @@ -7,7 +7,7 @@ from setuptools import setup, find_packages -VERSION = "2.0.6" +VERSION = "2.1.0" CLASSIFIERS = [ 'Development Status :: 4 - Beta',