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

Reply via email to