From 9764d8736dc630d6c550e487371488d54a97c5a4 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Tue, 10 Nov 2026 09:55:01 -0800 Subject: [PATCH] fix: harden archive extraction - extract tarballs with filter="data" to reject traversal, absolute paths, and escaping links - mask zip member modes to 0o777 so archives can't set setuid/setgid/sticky Co-Authored-By: Claude Opus 5.5 --- autotest/test_zip.py | 15 ++++++++++++++- modflow_devtools/download.py | 3 ++- modflow_devtools/zip.py | 3 ++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/autotest/test_zip.py b/autotest/test_zip.py index 91675c51..29fb5763 100644 --- a/autotest/test_zip.py +++ b/autotest/test_zip.py @@ -6,7 +6,7 @@ from pathlib import Path from pprint import pprint from shutil import which -from zipfile import ZipFile +from zipfile import ZipFile, ZipInfo import pytest @@ -74,3 +74,16 @@ def test_extractall_preserves_execute_permission(function_tmpdir, archive, mf): path = function_tmpdir / exe_name assert path.is_file() assert os.access(path, os.X_OK) == mf + + +@excludes_platform("Windows") +def test_extract_strips_special_mode_bits(function_tmpdir): + zip_path = function_tmpdir / "setuid.zip" + info = ZipInfo("prog") + info.external_attr = 0o104755 << 16 # regular file, setuid + 0755 + with ZipFile(zip_path, "w") as zf: + zf.writestr(info, b"#!/bin/sh\n") + + out = function_tmpdir / "out" + MFZipFile(zip_path).extractall(out) + assert (out / "prog").stat().st_mode & 0o7777 == 0o755 diff --git a/modflow_devtools/download.py b/modflow_devtools/download.py index d6388548..e5e10db3 100644 --- a/modflow_devtools/download.py +++ b/modflow_devtools/download.py @@ -332,7 +332,8 @@ def download_and_unzip( file_path.unlink() elif "tar" in file_path.suffix: ar = tarfile.open(file_path) - ar.extractall(path=str(path)) + # reject absolute paths, traversal, and links escaping the target dir + ar.extractall(path=str(path), filter="data") ar.close() # delete the zipfile diff --git a/modflow_devtools/zip.py b/modflow_devtools/zip.py index d09848da..2aa7f0ae 100644 --- a/modflow_devtools/zip.py +++ b/modflow_devtools/zip.py @@ -35,7 +35,8 @@ def extract(self, member, path=None, pwd=None): path = os.getcwd() ret_val = self._extract_member(member, str(path), pwd) - attr = member.external_attr >> 16 + # permission bits only, no setuid/setgid/sticky + attr = (member.external_attr >> 16) & 0o777 if attr != 0: os.chmod(ret_val, attr)