Commit 0425790c authored by Jason R. Coombs's avatar Jason R. Coombs Committed by GitHub

Merge pull request #1640 from pypa/bugfix/1635-disallow-parent-paths

Disallow parent path traversal in resource paths, part 1 (deprecation)
parents 97e8ad4f 6636302f
Resource paths are passed to ``pkg_resources.resource_string`` and similar no longer accept paths that traverse parents, that begin with a leading ``/``. Violations of this expectation raise DeprecationWarnings and will become errors. Additionally, any paths that are absolute on Windows are strictly disallowed and will raise ValueErrors.
...@@ -1132,8 +1132,8 @@ relative to the root of the identified distribution; i.e. its first path ...@@ -1132,8 +1132,8 @@ relative to the root of the identified distribution; i.e. its first path
segment will be treated as a peer of the top-level modules or packages in the segment will be treated as a peer of the top-level modules or packages in the
distribution. distribution.
Note that resource names must be ``/``-separated paths and cannot be absolute Note that resource names must be ``/``-separated paths rooted at the package,
(i.e. no leading ``/``) or contain relative names like ``".."``. Do *not* use cannot contain relative names like ``".."``, and cannot be absolute. Do *not* use
``os.path`` routines to manipulate resource paths, as they are *not* filesystem ``os.path`` routines to manipulate resource paths, as they are *not* filesystem
paths. paths.
......
...@@ -39,6 +39,8 @@ import tempfile ...@@ -39,6 +39,8 @@ import tempfile
import textwrap import textwrap
import itertools import itertools
import inspect import inspect
import ntpath
import posixpath
from pkgutil import get_importer from pkgutil import get_importer
try: try:
...@@ -1466,10 +1468,73 @@ class NullProvider: ...@@ -1466,10 +1468,73 @@ class NullProvider:
) )
def _fn(self, base, resource_name): def _fn(self, base, resource_name):
self._validate_resource_path(resource_name)
if resource_name: if resource_name:
return os.path.join(base, *resource_name.split('/')) return os.path.join(base, *resource_name.split('/'))
return base return base
@staticmethod
def _validate_resource_path(path):
"""
Validate the resource paths according to the docs.
https://setuptools.readthedocs.io/en/latest/pkg_resources.html#basic-resource-access
>>> warned = getfixture('recwarn')
>>> warnings.simplefilter('always')
>>> vrp = NullProvider._validate_resource_path
>>> vrp('foo/bar.txt')
>>> bool(warned)
False
>>> vrp('../foo/bar.txt')
>>> bool(warned)
True
>>> warned.clear()
>>> vrp('/foo/bar.txt')
>>> bool(warned)
True
>>> vrp('foo/../../bar.txt')
>>> bool(warned)
True
>>> warned.clear()
>>> vrp('foo/f../bar.txt')
>>> bool(warned)
False
Windows path separators are straight-up disallowed.
>>> vrp(r'\\foo/bar.txt')
Traceback (most recent call last):
...
ValueError: Use of .. or absolute path in a resource path \
is not allowed.
>>> vrp(r'C:\\foo/bar.txt')
Traceback (most recent call last):
...
ValueError: Use of .. or absolute path in a resource path \
is not allowed.
"""
invalid = (
os.path.pardir in path.split(posixpath.sep) or
posixpath.isabs(path) or
ntpath.isabs(path)
)
if not invalid:
return
msg = "Use of .. or absolute path in a resource path is not allowed."
# Aggressively disallow Windows absolute paths
if ntpath.isabs(path) and not posixpath.isabs(path):
raise ValueError(msg)
# for compatibility, warn; in future
# raise ValueError(msg)
warnings.warn(
msg[:-1] + " and will raise exceptions in a future release.",
DeprecationWarning,
stacklevel=4,
)
def _get(self, path): def _get(self, path):
if hasattr(self.loader, 'get_data'): if hasattr(self.loader, 'get_data'):
return self.loader.get_data(path) return self.loader.get_data(path)
...@@ -1888,7 +1953,7 @@ def find_eggs_in_zip(importer, path_item, only=False): ...@@ -1888,7 +1953,7 @@ def find_eggs_in_zip(importer, path_item, only=False):
if only: if only:
# don't yield nested distros # don't yield nested distros
return return
for subitem in metadata.resource_listdir('/'): for subitem in metadata.resource_listdir(''):
if _is_egg_path(subitem): if _is_egg_path(subitem):
subpath = os.path.join(path_item, subitem) subpath = os.path.join(path_item, subitem)
dists = find_eggs_in_zip(zipimport.zipimporter(subpath), subpath) dists = find_eggs_in_zip(zipimport.zipimporter(subpath), subpath)
......
...@@ -93,7 +93,6 @@ class TestZipProvider: ...@@ -93,7 +93,6 @@ class TestZipProvider:
expected_root = ['data.dat', 'mod.py', 'subdir'] expected_root = ['data.dat', 'mod.py', 'subdir']
assert sorted(zp.resource_listdir('')) == expected_root assert sorted(zp.resource_listdir('')) == expected_root
assert sorted(zp.resource_listdir('/')) == expected_root
expected_subdir = ['data2.dat', 'mod2.py'] expected_subdir = ['data2.dat', 'mod2.py']
assert sorted(zp.resource_listdir('subdir')) == expected_subdir assert sorted(zp.resource_listdir('subdir')) == expected_subdir
...@@ -106,7 +105,6 @@ class TestZipProvider: ...@@ -106,7 +105,6 @@ class TestZipProvider:
zp2 = pkg_resources.ZipProvider(mod2) zp2 = pkg_resources.ZipProvider(mod2)
assert sorted(zp2.resource_listdir('')) == expected_subdir assert sorted(zp2.resource_listdir('')) == expected_subdir
assert sorted(zp2.resource_listdir('/')) == expected_subdir
assert zp2.resource_listdir('subdir') == [] assert zp2.resource_listdir('subdir') == []
assert zp2.resource_listdir('subdir/') == [] assert zp2.resource_listdir('subdir/') == []
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment