From b99e2bfb8c1b1f61377193d51cf627689ec62606 Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
Date: Wed, 18 Mar 2026 17:31:01 +0000
Subject: [PATCH] gh-146121: `pkgutil.get_data()` reject invalid resource
 arguments (GH-146122) (cherry picked from commit
 bcdf231946b1da8bdfbab4c05539bb0cc964a1c7)

Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
---
 Doc/library/pkgutil.rst                                                  |   16 +++++++-
 Lib/pkgutil.py                                                           |    3 +
 Lib/test/test_pkgutil.py                                                 |   19 ++++++++++
 Misc/NEWS.d/next/Security/2026-03-16-18-07-00.gh-issue-146121.vRbdro.rst |    3 +
 4 files changed, 39 insertions(+), 2 deletions(-)
 create mode 100644 Misc/NEWS.d/next/Security/2026-03-16-18-07-00.gh-issue-146121.vRbdro.rst

Index: Python-2.7.18/Doc/library/pkgutil.rst
===================================================================
--- Python-2.7.18.orig/Doc/library/pkgutil.rst	2020-04-19 23:13:39.000000000 +0200
+++ Python-2.7.18/Doc/library/pkgutil.rst	2026-04-07 23:36:13.466914152 +0200
@@ -176,8 +176,7 @@
    This is a wrapper for the :pep:`302` loader :func:`get_data` API.  The
    *package* argument should be the name of a package, in standard module format
    (``foo.bar``).  The *resource* argument should be in the form of a relative
-   filename, using ``/`` as the path separator.  The parent directory name
-   ``..`` is not allowed, and nor is a rooted name (starting with a ``/``).
+   filename, using ``/`` as the path separator.
 
    The function returns a binary string that is the contents of the specified
    resource.
@@ -188,6 +187,19 @@
       d = os.path.dirname(sys.modules[package].__file__)
       data = open(os.path.join(d, resource), 'rb').read()
 
+   Like the :func:`open` function, :func:`!get_data` can follow parent
+   directories (``../``) and absolute paths (starting with ``/`` or ``C:/``,
+   for example).
+
+   .. warning::
+
+      This function is intended for trusted input.
+      It does not verify that *resource* "belongs" to *package*.
+
+   If you use a user-provided *resource* path, consider verifying it.
+   For example, require an alphanumeric filename with a known extension, or
+   install and check a list of known resources.
+
    If the package cannot be located or loaded, or it uses a :pep:`302` loader
    which does not support :func:`get_data`, then ``None`` is returned.
 
Index: Python-2.7.18/Lib/pkgutil.py
===================================================================
--- Python-2.7.18.orig/Lib/pkgutil.py	2020-04-19 23:13:39.000000000 +0200
+++ Python-2.7.18/Lib/pkgutil.py	2026-04-07 23:33:30.979403271 +0200
@@ -584,6 +584,9 @@
     # signature - an os.path format "filename" starting with the dirname of
     # the package's __file__
     parts = resource.split('/')
+    if os.path.isabs(resource) or '..' in parts:
+        raise ValueError("resource must be a relative path with no "
+                         "parent directory components")
     parts.insert(0, os.path.dirname(mod.__file__))
     resource_name = os.path.join(*parts)
     return loader.get_data(resource_name)
Index: Python-2.7.18/Lib/test/test_pkgutil.py
===================================================================
--- Python-2.7.18.orig/Lib/test/test_pkgutil.py	2020-04-19 23:13:39.000000000 +0200
+++ Python-2.7.18/Lib/test/test_pkgutil.py	2026-04-07 23:33:30.979767878 +0200
@@ -50,6 +50,25 @@
 
         del sys.modules[pkg]
 
+    def test_getdata_path_traversal(self):
+        pkg = 'test_getdata_traversal'
+
+        # Make a package with some resources
+        package_dir = os.path.join(self.dirname, pkg)
+        os.mkdir(package_dir)
+        # Empty init.py
+        f = open(os.path.join(package_dir, '__init__.py'), "wb")
+        f.close()
+
+        with self.assertRaises(ValueError):
+            pkgutil.get_data(pkg, '../../../etc/passwd')
+        with self.assertRaises(ValueError):
+            pkgutil.get_data(pkg, 'sub/../../../etc/passwd')
+        with self.assertRaises(ValueError):
+            pkgutil.get_data(pkg, os.path.abspath('/etc/passwd'))
+
+        del sys.modules[pkg]
+
     def test_getdata_zipfile(self):
         zip = 'test_getdata_zipfile.zip'
         pkg = 'test_getdata_zipfile'
Index: Python-2.7.18/Misc/NEWS.d/next/Security/2026-03-16-18-07-00.gh-issue-146121.vRbdro.rst
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ Python-2.7.18/Misc/NEWS.d/next/Security/2026-03-16-18-07-00.gh-issue-146121.vRbdro.rst	2026-04-07 23:33:30.980041193 +0200
@@ -0,0 +1,3 @@
+:func:`pkgutil.get_data` now raises rejects *resource* arguments containing the
+parent directory components or that is an absolute path.
+This addresses :cve:`2026-3479`.
