Version in base suite: 5.2.0+dfsg-2 Base version: gdown_5.2.0+dfsg-2 Target version: gdown_5.2.0+dfsg-2+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/g/gdown/gdown_5.2.0+dfsg-2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/g/gdown/gdown_5.2.0+dfsg-2+deb13u1.dsc changelog | 7 patches/0001-fix-prevent-path-traversal-in-archive-extraction-and.patch | 195 ++++++++++ patches/series | 1 3 files changed, 203 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmphxxncn23/gdown_5.2.0+dfsg-2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmphxxncn23/gdown_5.2.0+dfsg-2+deb13u1.dsc: no acceptable signature found diff -Nru gdown-5.2.0+dfsg/debian/changelog gdown-5.2.0+dfsg/debian/changelog --- gdown-5.2.0+dfsg/debian/changelog 2025-01-10 19:46:54.000000000 +0000 +++ gdown-5.2.0+dfsg/debian/changelog 2026-06-09 20:11:29.000000000 +0000 @@ -1,3 +1,10 @@ +gdown (5.2.0+dfsg-2+deb13u1) trixie; urgency=medium + + * Non-maintainer upload. + * CVE-2026-40491: Arbitrary File Write via Path Traversal + + -- Adrian Bunk Tue, 09 Jun 2026 23:11:29 +0300 + gdown (5.2.0+dfsg-2) unstable; urgency=medium * Source-only upload to allow package to migrate to testing. diff -Nru gdown-5.2.0+dfsg/debian/patches/0001-fix-prevent-path-traversal-in-archive-extraction-and.patch gdown-5.2.0+dfsg/debian/patches/0001-fix-prevent-path-traversal-in-archive-extraction-and.patch --- gdown-5.2.0+dfsg/debian/patches/0001-fix-prevent-path-traversal-in-archive-extraction-and.patch 1970-01-01 00:00:00.000000000 +0000 +++ gdown-5.2.0+dfsg/debian/patches/0001-fix-prevent-path-traversal-in-archive-extraction-and.patch 2026-06-09 20:10:34.000000000 +0000 @@ -0,0 +1,195 @@ +From 62da8ca8de8faef7c875b1a221413243e96737b8 Mon Sep 17 00:00:00 2001 +From: Kentaro Wada +Date: Sun, 12 Apr 2026 14:47:54 +0900 +Subject: fix: prevent path traversal in archive extraction and filename + handling + +- Add path validation to extractall() to prevent zip/tar slip attacks +- Add _sanitize_filename() to neutralize path separators, null bytes, + and dot-dot sequences in filenames from responses and URLs +- Sanitize root folder name in download_folder() before building paths +- Use Python 3.12+ data filter for tar extraction when available, + with manual link/special-file/traversal checks for older versions + +Fixes GHSA-76hw-p97h-883f +--- + gdown/download.py | 16 ++++++--- + gdown/download_folder.py | 5 ++- + gdown/extractall.py | 72 ++++++++++++++++++++++++++++++---------- + 3 files changed, 70 insertions(+), 23 deletions(-) + +diff --git a/gdown/download.py b/gdown/download.py +index baadef1..4ba6c6d 100644 +--- a/gdown/download.py ++++ b/gdown/download.py +@@ -61,18 +61,24 @@ def get_url_from_gdrive_confirmation(contents): + return url + + ++def _sanitize_filename(filename): ++ filename = filename.replace("\x00", "") ++ filename = filename.replace("/", "_").replace("\\", "_").strip() ++ if filename in ("", ".", ".."): ++ return "_" ++ return filename ++ ++ + def _get_filename_from_response(response): + content_disposition = urllib.parse.unquote(response.headers["Content-Disposition"]) + + m = re.search(r"filename\*=UTF-8''(.*)", content_disposition) + if m: +- filename = m.groups()[0] +- return filename.replace(osp.sep, "_") ++ return _sanitize_filename(m.groups()[0]) + + m = re.search('attachment; filename="(.*?)"', content_disposition) + if m: +- filename = m.groups()[0] +- return filename ++ return _sanitize_filename(m.groups()[0]) + + return None + +@@ -283,7 +289,7 @@ def download( + filename_from_url = _get_filename_from_response(response=res) + last_modified_time = _get_modified_time_from_response(response=res) + if filename_from_url is None: +- filename_from_url = osp.basename(url) ++ filename_from_url = _sanitize_filename(osp.basename(url)) + + if output is None: + output = filename_from_url +diff --git a/gdown/download_folder.py b/gdown/download_folder.py +index 8c59c96..f38dadb 100644 +--- a/gdown/download_folder.py ++++ b/gdown/download_folder.py +@@ -12,6 +12,7 @@ from typing import Union + import bs4 + + from .download import _get_session ++from .download import _sanitize_filename + from .download import download + from .exceptions import FolderContentsMaximumLimitError + from .parse_url import is_google_drive_url +@@ -182,7 +183,7 @@ def _get_directory_structure(gdrive_file, previous_path): + + directory_structure = [] + for file in gdrive_file.children: +- file.name = file.name.replace(osp.sep, "_") ++ file.name = _sanitize_filename(file.name) + if file.is_folder(): + directory_structure.append((None, osp.join(previous_path, file.name))) + for i in _get_directory_structure(file, osp.join(previous_path, file.name)): +@@ -283,6 +284,8 @@ def download_folder( + print("Failed to retrieve folder contents", file=sys.stderr) + return None + ++ gdrive_file.name = _sanitize_filename(gdrive_file.name) ++ + if not quiet: + print("Retrieving folder contents completed", file=sys.stderr) + print("Building directory structure", file=sys.stderr) +diff --git a/gdown/extractall.py b/gdown/extractall.py +index 6846026..dd22427 100644 +--- a/gdown/extractall.py ++++ b/gdown/extractall.py +@@ -1,8 +1,16 @@ ++import os + import os.path as osp ++import sys + import tarfile + import zipfile + + ++def _is_within_directory(directory, target): ++ abs_directory = osp.realpath(directory) ++ abs_target = osp.realpath(target) ++ return abs_target.startswith(abs_directory + os.sep) or abs_target == abs_directory ++ ++ + def extractall(path, to=None): + """Extract archive file. + +@@ -18,31 +26,61 @@ def extractall(path, to=None): + to = osp.dirname(path) + + if path.endswith(".zip"): +- opener, mode = zipfile.ZipFile, "r" +- elif path.endswith(".tar"): +- opener, mode = tarfile.open, "r" ++ return _extractall_zip(path, to) ++ ++ if path.endswith(".tar"): ++ tar_mode = "r" + elif path.endswith(".tar.gz") or path.endswith(".tgz"): +- opener, mode = tarfile.open, "r:gz" ++ tar_mode = "r:gz" + elif path.endswith(".tar.bz2") or path.endswith(".tbz"): +- opener, mode = tarfile.open, "r:bz2" ++ tar_mode = "r:bz2" + else: + raise ValueError( + "Could not extract '%s' as no appropriate " "extractor is found" % path + ) + +- def namelist(f): +- if isinstance(f, zipfile.ZipFile): +- return f.namelist() +- return [m.path for m in f.members] ++ return _extractall_tar(path, to, tar_mode) + +- def filelist(f): +- files = [] +- for fname in namelist(f): +- fname = osp.join(to, fname) +- files.append(fname) +- return files + +- with opener(path, mode) as f: ++def _extractall_zip(path, to): ++ with zipfile.ZipFile(path, "r") as f: ++ names = f.namelist() ++ for member in names: ++ member_path = osp.join(to, member) ++ if not _is_within_directory(to, member_path): ++ raise ValueError( ++ "Archive member '%s' would extract outside " ++ "target directory: %s" % (member, to) ++ ) + f.extractall(path=to) ++ return [osp.join(to, name) for name in names] ++ ++ ++def _extractall_tar(path, to, tar_mode): ++ with tarfile.open(path, tar_mode) as f: ++ members = f.getmembers() ++ if sys.version_info >= (3, 12): ++ f.extractall(path=to, filter="data") ++ else: ++ for member in members: ++ if member.issym() or member.islnk(): ++ raise ValueError( ++ "Archive member '%s' is a link, " ++ "which is not allowed for security reasons" ++ % member.name ++ ) ++ if member.ischr() or member.isblk() or member.isfifo(): ++ raise ValueError( ++ "Archive member '%s' is a special file, " ++ "which is not allowed for security reasons" ++ % member.name ++ ) ++ member_path = osp.join(to, member.name) ++ if not _is_within_directory(to, member_path): ++ raise ValueError( ++ "Archive member '%s' would extract outside " ++ "target directory: %s" % (member.name, to) ++ ) ++ f.extractall(path=to) + +- return filelist(f) ++ return [osp.join(to, m.path) for m in members] +-- +2.47.3 + diff -Nru gdown-5.2.0+dfsg/debian/patches/series gdown-5.2.0+dfsg/debian/patches/series --- gdown-5.2.0+dfsg/debian/patches/series 1970-01-01 00:00:00.000000000 +0000 +++ gdown-5.2.0+dfsg/debian/patches/series 2026-06-09 20:11:29.000000000 +0000 @@ -0,0 +1 @@ +0001-fix-prevent-path-traversal-in-archive-extraction-and.patch