--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected], [email protected]
Control: affects -1 + src:gdown
User: [email protected]
Usertags: pu
* CVE-2026-40491: Arbitrary File Write via Path Traversal
diffstat for gdown-5.2.0+dfsg gdown-5.2.0+dfsg
changelog | 7
patches/0001-fix-prevent-path-traversal-in-archive-extraction-and.patch | 195
++++++++++
patches/series | 1
3 files changed, 203 insertions(+)
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 21:46:54.000000000 +0200
+++ gdown-5.2.0+dfsg/debian/changelog 2026-06-09 23:11:29.000000000 +0300
@@ -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 <[email protected]> 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 02:00:00.000000000 +0200
+++
gdown-5.2.0+dfsg/debian/patches/0001-fix-prevent-path-traversal-in-archive-extraction-and.patch
2026-06-09 23:10:34.000000000 +0300
@@ -0,0 +1,195 @@
+From 62da8ca8de8faef7c875b1a221413243e96737b8 Mon Sep 17 00:00:00 2001
+From: Kentaro Wada <[email protected]>
+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 02:00:00.000000000
+0200
+++ gdown-5.2.0+dfsg/debian/patches/series 2026-06-09 23:11:29.000000000
+0300
@@ -0,0 +1 @@
+0001-fix-prevent-path-traversal-in-archive-extraction-and.patch
--- End Message ---