Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-15-SP2:GA
python-setuptools.35197
CVE-2024-6345-code-execution-via-download-funcs...
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File CVE-2024-6345-code-execution-via-download-funcs.patch of Package python-setuptools.35197
Index: setuptools-40.5.0/setuptools/package_index.py =================================================================== --- setuptools-40.5.0.orig/setuptools/package_index.py +++ setuptools-40.5.0/setuptools/package_index.py @@ -1,5 +1,6 @@ """PyPI and direct package downloading""" import sys +import subprocess import os import re import shutil @@ -576,7 +577,7 @@ class PackageIndex(Environment): scheme = URL_SCHEME(spec) if scheme: # It's a url, download it to tmpdir - found = self._download_url(scheme.group(1), spec, tmpdir) + found = self._download_url(spec, tmpdir) base, fragment = egg_info_for_url(spec) if base.endswith('.py'): found = self.gen_setup(found, fragment, tmpdir) @@ -794,7 +795,7 @@ class PackageIndex(Environment): raise DistutilsError("Download error for %s: %s" % (url, v)) - def _download_url(self, scheme, url, tmpdir): + def _download_url(self, url, tmpdir): # Determine download filename # name, fragment = egg_info_for_url(url) @@ -809,19 +810,75 @@ class PackageIndex(Environment): filename = os.path.join(tmpdir, name) - # Download the file - # - if scheme == 'svn' or scheme.startswith('svn+'): - return self._download_svn(url, filename) - elif scheme == 'git' or scheme.startswith('git+'): - return self._download_git(url, filename) - elif scheme.startswith('hg+'): - return self._download_hg(url, filename) - elif scheme == 'file': - return urllib.request.url2pathname(urllib.parse.urlparse(url)[2]) + return self._download_vcs(url, filename) or self._download_other(url, filename) + + @staticmethod + def _resolve_vcs(url): + """ + >>> rvcs = PackageIndex._resolve_vcs + >>> rvcs('git+http://foo/bar') + 'git' + >>> rvcs('hg+https://foo/bar') + 'hg' + >>> rvcs('git:myhost') + 'git' + >>> rvcs('hg:myhost') + >>> rvcs('http://foo/bar') + """ + scheme = urllib.parse.urlsplit(url).scheme + pre, sep, post = scheme.partition('+') + # svn and git have their own protocol; hg does not + allowed = set(['svn', 'git'] + ['hg'] * bool(sep)) + return next(iter({pre} & allowed), None) + + def _download_vcs(self, url, spec_filename): + vcs = self._resolve_vcs(url) + if not vcs: + return + if vcs == 'svn': + warnings.warn("SVN download support is deprecated", UserWarning) + + filename, _, _ = spec_filename.partition('#') + url, rev = self._vcs_split_rev_from_url(url) + svn_creds = [] + if url.lower().startswith('svn:') and '@' in url: + parsed_url = urllib.parse.urlparse(url) + if parsed_url.username and parsed_url.password: + svn_creds.extend( + ["--username", parsed_url.username, + "--password", parsed_url.password]) + elif parsed_url.username and not parsed_url.password: + svn_creds.extend(["--username", parsed_url.username]) + # We need to remove the auth from the URL + domain = parsed_url.netloc.split('@')[1] + parsed_url = parsed_url._replace(netloc=domain) + url = urllib.parse.urlunparse(parsed_url) + + self.info("Doing %s clone from %s to %s" % (vcs, url, filename)) + if vcs == 'svn': + cmd_line = [vcs, 'checkout', '-q'] + svn_creds + [url, filename] + subprocess.check_call(cmd_line) + return filename else: - self.url_ok(url, True) # raises error if not allowed - return self._attempt_download(url, filename) + subprocess.check_call([vcs, 'clone', '--quiet', url, filename]) + + co_commands = dict( + git=[vcs, '-C', filename, 'checkout', '--quiet', rev], + hg=[vcs, '--cwd', filename, 'up', '-C', '-r', rev, '-q'], + ) + if rev is not None: + self.info("Checking out %s" % rev) + subprocess.check_call(co_commands[vcs]) + + return filename + + def _download_other(self, url, filename): + scheme = urllib.parse.urlsplit(url).scheme + if scheme == 'file': # pragma: no cover + return urllib.request.url2pathname(urllib.parse.urlparse(url).path) + # raise error if not allowed + self.url_ok(url, True) + return self._attempt_download(url, filename) def scan_url(self, url): self.process_url(url, True) @@ -871,54 +928,34 @@ class PackageIndex(Environment): return filename @staticmethod - def _vcs_split_rev_from_url(url, pop_prefix=False): - scheme, netloc, path, query, frag = urllib.parse.urlsplit(url) + def _vcs_split_rev_from_url(url,): + """ + Given a possible VCS URL, return a clean URL and resolved revision if any. + >>> vsrfu = PackageIndex._vcs_split_rev_from_url + >>> vsrfu('git+https://github.com/pypa/setuptools@v69.0.0#egg-info=setuptools') + ('https://github.com/pypa/setuptools', 'v69.0.0') + >>> vsrfu('git+https://github.com/pypa/setuptools#egg-info=setuptools') + ('https://github.com/pypa/setuptools', None) + >>> vsrfu('http://foo/bar') + ('http://foo/bar', None) + """ + parts = urllib.parse.urlsplit(url) - scheme = scheme.split('+', 1)[-1] + clean_scheme = parts.scheme.split('+', 1)[-1] # Some fragment identification fails - path = path.split('#', 1)[0] - - rev = None - if '@' in path: - path, rev = path.rsplit('@', 1) - - # Also, discard fragment - url = urllib.parse.urlunsplit((scheme, netloc, path, query, '')) + no_fragment_path, _, _ = parts.path.partition('#') - return url, rev - - def _download_git(self, url, filename): - filename = filename.split('#', 1)[0] - url, rev = self._vcs_split_rev_from_url(url, pop_prefix=True) - - self.info("Doing git clone from %s to %s", url, filename) - os.system("git clone --quiet %s %s" % (url, filename)) - - if rev is not None: - self.info("Checking out %s", rev) - os.system("(cd %s && git checkout --quiet %s)" % ( - filename, - rev, - )) + pre, sep, post = no_fragment_path.rpartition('@') + clean_path, rev = (pre, post) if sep else (post, None) - return filename - - def _download_hg(self, url, filename): - filename = filename.split('#', 1)[0] - url, rev = self._vcs_split_rev_from_url(url, pop_prefix=True) - - self.info("Doing hg clone from %s to %s", url, filename) - os.system("hg clone --quiet %s %s" % (url, filename)) - - if rev is not None: - self.info("Updating to %s", rev) - os.system("(cd %s && hg up -C -r %s -q)" % ( - filename, - rev, - )) - - return filename + resolved = parts._replace( + scheme=clean_scheme, + path=clean_path, + # discard the fragment + fragment='', + ).geturl() + return resolved, rev def debug(self, msg, *args): log.debug(msg, *args) Index: setuptools-40.5.0/setuptools/tests/test_packageindex.py =================================================================== --- setuptools-40.5.0.orig/setuptools/tests/test_packageindex.py +++ setuptools-40.5.0/setuptools/tests/test_packageindex.py @@ -3,6 +3,7 @@ from __future__ import absolute_import import sys import os import distutils.errors +import mock from setuptools.extern import six from setuptools.extern.six.moves import urllib, http_client @@ -223,6 +224,65 @@ class TestPackageIndex: assert dists[0].version == '' assert dists[1].version == vc + def test_download_git_with_rev(self, tmpdir): + url = 'git+https://github.example/group/project@master#egg=foo' + index = setuptools.package_index.PackageIndex() + expected_dir = os.path.join(str(tmpdir), 'project@master') + with mock.patch("subprocess.check_call") as subprocess_mock: + result = index.download(url, str(tmpdir)) + expected_clone = mock.call([ + 'git', 'clone', '--quiet', 'https://github.example/group/project', + expected_dir, + ]) + expected_checkout = mock.call([ + 'git', '-C', expected_dir, 'checkout', '--quiet', 'master', + ]) + subprocess_mock.assert_has_calls((expected_clone, expected_checkout)) + assert subprocess_mock.call_count == 2 + assert result == expected_dir + + def test_download_git_no_rev(self, tmpdir): + url = 'git+https://github.example/group/project#egg=foo' + index = setuptools.package_index.PackageIndex() + expected_dir = os.path.join(str(tmpdir), 'project') + with mock.patch("subprocess.check_call") as subprocess_mock: + result = index.download(url, str(tmpdir)) + expected_clone = [ + 'git', 'clone', '--quiet', 'https://github.example/group/project', + expected_dir, + ] + subprocess_mock.assert_called_once_with(expected_clone) + assert subprocess_mock.call_count == 1 + assert result == expected_dir + + def test_download_svn_with_username_no_password(self, tmpdir): + url = 'svn://user@example.com/repository/project/trunk' + index = setuptools.package_index.PackageIndex() + expected_dir = os.path.join(str(tmpdir), 'trunk') + with mock.patch("subprocess.check_call") as subprocess_mock: + result = index.download(url, str(tmpdir)) + expected_clone = [ + 'svn', 'checkout', '-q', '--username', 'user', + 'svn://example.com/repository/project/trunk', + expected_dir, + ] + subprocess_mock.assert_called_once_with(expected_clone) + assert subprocess_mock.call_count == 1 + + def test_download_svn_with_username_and_password(self, tmpdir): + url = 'svn://user:pass@example.com/repository/project/trunk' + index = setuptools.package_index.PackageIndex() + expected_dir = os.path.join(str(tmpdir), 'trunk') + with mock.patch("subprocess.check_call") as subprocess_mock: + result = index.download(url, str(tmpdir)) + expected_clone = [ + 'svn', 'checkout', '-q', '--username', 'user', + '--password', 'pass', 'svn://example.com/repository/project/trunk', + expected_dir, + ] + subprocess_mock.assert_called_once_with(expected_clone) + assert subprocess_mock.call_count == 1 + class TestContentCheckers: def test_md5(self):
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor