import UBI python3.11-3.11.5-1.el8_9
This commit is contained in:
parent
574d4c6c83
commit
fbce823374
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1 @@
|
|||||||
SOURCES/Python-3.11.2.tar.xz
|
SOURCES/Python-3.11.5.tar.xz
|
||||||
|
@ -1 +1 @@
|
|||||||
ae1c199ecb7a969588b15354e19e7b60cb65d1b9 SOURCES/Python-3.11.2.tar.xz
|
b13ec58fa6ebf5b0f7178555c5506e135cb7d785 SOURCES/Python-3.11.5.tar.xz
|
||||||
|
251
SOURCES/00397-tarfile-filter.patch
Normal file
251
SOURCES/00397-tarfile-filter.patch
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
From 8b70605b594b3831331a9340ba764ff751871612 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Petr Viktorin <encukou@gmail.com>
|
||||||
|
Date: Mon, 6 Mar 2023 17:24:24 +0100
|
||||||
|
Subject: [PATCH] CVE-2007-4559, PEP-706: Add filters for tarfile extraction
|
||||||
|
(downstream)
|
||||||
|
|
||||||
|
Add and test RHEL-specific ways of configuring the default behavior: environment
|
||||||
|
variable and config file.
|
||||||
|
---
|
||||||
|
Lib/tarfile.py | 42 +++++++++++++
|
||||||
|
Lib/test/test_shutil.py | 3 +-
|
||||||
|
Lib/test/test_tarfile.py | 128 ++++++++++++++++++++++++++++++++++++++-
|
||||||
|
3 files changed, 169 insertions(+), 4 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/Lib/tarfile.py b/Lib/tarfile.py
|
||||||
|
index 130b5e0..3b7d8d5 100755
|
||||||
|
--- a/Lib/tarfile.py
|
||||||
|
+++ b/Lib/tarfile.py
|
||||||
|
@@ -72,6 +72,13 @@ __all__ = ["TarFile", "TarInfo", "is_tarfile", "TarError", "ReadError",
|
||||||
|
"ENCODING", "USTAR_FORMAT", "GNU_FORMAT", "PAX_FORMAT",
|
||||||
|
"DEFAULT_FORMAT", "open"]
|
||||||
|
|
||||||
|
+# If true, use the safer (but backwards-incompatible) 'tar' extraction filter,
|
||||||
|
+# rather than 'fully_trusted', by default.
|
||||||
|
+# The emitted warning is changed to match.
|
||||||
|
+_RH_SAFER_DEFAULT = True
|
||||||
|
+
|
||||||
|
+# System-wide configuration file
|
||||||
|
+_CONFIG_FILENAME = '/etc/python/tarfile.cfg'
|
||||||
|
|
||||||
|
#---------------------------------------------------------
|
||||||
|
# tar constants
|
||||||
|
@@ -2211,6 +2218,41 @@ class TarFile(object):
|
||||||
|
if filter is None:
|
||||||
|
filter = self.extraction_filter
|
||||||
|
if filter is None:
|
||||||
|
+ name = os.environ.get('PYTHON_TARFILE_EXTRACTION_FILTER')
|
||||||
|
+ if name is None:
|
||||||
|
+ try:
|
||||||
|
+ file = bltn_open(_CONFIG_FILENAME)
|
||||||
|
+ except FileNotFoundError:
|
||||||
|
+ pass
|
||||||
|
+ else:
|
||||||
|
+ import configparser
|
||||||
|
+ conf = configparser.ConfigParser(
|
||||||
|
+ interpolation=None,
|
||||||
|
+ comment_prefixes=('#', ),
|
||||||
|
+ )
|
||||||
|
+ with file:
|
||||||
|
+ conf.read_file(file)
|
||||||
|
+ name = conf.get('tarfile',
|
||||||
|
+ 'PYTHON_TARFILE_EXTRACTION_FILTER',
|
||||||
|
+ fallback='')
|
||||||
|
+ if name:
|
||||||
|
+ try:
|
||||||
|
+ filter = _NAMED_FILTERS[name]
|
||||||
|
+ except KeyError:
|
||||||
|
+ raise ValueError(f"filter {filter!r} not found") from None
|
||||||
|
+ self.extraction_filter = filter
|
||||||
|
+ return filter
|
||||||
|
+ if _RH_SAFER_DEFAULT:
|
||||||
|
+ warnings.warn(
|
||||||
|
+ 'The default behavior of tarfile extraction has been '
|
||||||
|
+ + 'changed to disallow common exploits '
|
||||||
|
+ + '(including CVE-2007-4559). '
|
||||||
|
+ + 'By default, absolute/parent paths are disallowed '
|
||||||
|
+ + 'and some mode bits are cleared. '
|
||||||
|
+ + 'See https://access.redhat.com/articles/7004769 '
|
||||||
|
+ + 'for more details.',
|
||||||
|
+ RuntimeWarning)
|
||||||
|
+ return tar_filter
|
||||||
|
return fully_trusted_filter
|
||||||
|
if isinstance(filter, str):
|
||||||
|
raise TypeError(
|
||||||
|
diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py
|
||||||
|
index 9bf4145..f247b82 100644
|
||||||
|
--- a/Lib/test/test_shutil.py
|
||||||
|
+++ b/Lib/test/test_shutil.py
|
||||||
|
@@ -1665,7 +1665,8 @@ class TestArchives(BaseTest, unittest.TestCase):
|
||||||
|
def check_unpack_tarball(self, format):
|
||||||
|
self.check_unpack_archive(format, filter='fully_trusted')
|
||||||
|
self.check_unpack_archive(format, filter='data')
|
||||||
|
- with warnings_helper.check_no_warnings(self):
|
||||||
|
+ with warnings_helper.check_warnings(
|
||||||
|
+ ('.*CVE-2007-4559', RuntimeWarning)):
|
||||||
|
self.check_unpack_archive(format)
|
||||||
|
|
||||||
|
def test_unpack_archive_tar(self):
|
||||||
|
diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py
|
||||||
|
index cdea033..4724285 100644
|
||||||
|
--- a/Lib/test/test_tarfile.py
|
||||||
|
+++ b/Lib/test/test_tarfile.py
|
||||||
|
@@ -2,7 +2,7 @@ import sys
|
||||||
|
import os
|
||||||
|
import io
|
||||||
|
from hashlib import sha256
|
||||||
|
-from contextlib import contextmanager
|
||||||
|
+from contextlib import contextmanager, ExitStack
|
||||||
|
from random import Random
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
@@ -2999,7 +2999,11 @@ class NoneInfoExtractTests(ReadTest):
|
||||||
|
tar = tarfile.open(tarname, mode='r', encoding="iso8859-1")
|
||||||
|
cls.control_dir = pathlib.Path(TEMPDIR) / "extractall_ctrl"
|
||||||
|
tar.errorlevel = 0
|
||||||
|
- tar.extractall(cls.control_dir, filter=cls.extraction_filter)
|
||||||
|
+ with ExitStack() as cm:
|
||||||
|
+ if cls.extraction_filter is None:
|
||||||
|
+ cm.enter_context(warnings.catch_warnings())
|
||||||
|
+ warnings.simplefilter(action="ignore", category=RuntimeWarning)
|
||||||
|
+ tar.extractall(cls.control_dir, filter=cls.extraction_filter)
|
||||||
|
tar.close()
|
||||||
|
cls.control_paths = set(
|
||||||
|
p.relative_to(cls.control_dir)
|
||||||
|
@@ -3674,7 +3678,8 @@ class TestExtractionFilters(unittest.TestCase):
|
||||||
|
"""Ensure the default filter does not warn (like in 3.12)"""
|
||||||
|
with ArchiveMaker() as arc:
|
||||||
|
arc.add('foo')
|
||||||
|
- with warnings_helper.check_no_warnings(self):
|
||||||
|
+ with warnings_helper.check_warnings(
|
||||||
|
+ ('.*CVE-2007-4559', RuntimeWarning)):
|
||||||
|
with self.check_context(arc.open(), None):
|
||||||
|
self.expect_file('foo')
|
||||||
|
|
||||||
|
@@ -3844,6 +3849,123 @@ class TestExtractionFilters(unittest.TestCase):
|
||||||
|
self.expect_exception(TypeError) # errorlevel is not int
|
||||||
|
|
||||||
|
|
||||||
|
+ @contextmanager
|
||||||
|
+ def rh_config_context(self, config_lines=None):
|
||||||
|
+ """Set up for testing various ways of overriding the default filter
|
||||||
|
+
|
||||||
|
+ return a triple with:
|
||||||
|
+ - temporary directory
|
||||||
|
+ - EnvironmentVarGuard()
|
||||||
|
+ - a test archive for use with check_* methods below
|
||||||
|
+
|
||||||
|
+ If config_lines is given, write them to the config file. Otherwise
|
||||||
|
+ the config file is missing.
|
||||||
|
+ """
|
||||||
|
+ tempdir = pathlib.Path(TEMPDIR) / 'tmp'
|
||||||
|
+ configfile = tempdir / 'tarfile.cfg'
|
||||||
|
+ with ArchiveMaker() as arc:
|
||||||
|
+ arc.add('good')
|
||||||
|
+ arc.add('ugly', symlink_to='/etc/passwd')
|
||||||
|
+ arc.add('../bad')
|
||||||
|
+ with (
|
||||||
|
+ os_helper.temp_dir(tempdir),
|
||||||
|
+ support.swap_attr(tarfile, '_CONFIG_FILENAME', str(configfile)),
|
||||||
|
+ os_helper.EnvironmentVarGuard() as env,
|
||||||
|
+ arc.open() as tar,
|
||||||
|
+ ):
|
||||||
|
+ if config_lines is not None:
|
||||||
|
+ with configfile.open('w') as f:
|
||||||
|
+ for line in config_lines:
|
||||||
|
+ print(line, file=f)
|
||||||
|
+ yield tempdir, env, tar
|
||||||
|
+
|
||||||
|
+ def check_rh_default_behavior(self, tar, tempdir):
|
||||||
|
+ """Check RH default: warn and refuse to extract dangerous files."""
|
||||||
|
+ with (
|
||||||
|
+ warnings_helper.check_warnings(
|
||||||
|
+ ('.*CVE-2007-4559', RuntimeWarning)),
|
||||||
|
+ self.assertRaises(tarfile.OutsideDestinationError),
|
||||||
|
+ ):
|
||||||
|
+ tar.extractall(tempdir / 'outdir')
|
||||||
|
+
|
||||||
|
+ def check_trusted_default(self, tar, tempdir):
|
||||||
|
+ """Check 'fully_trusted' is configured as the default filter."""
|
||||||
|
+ with (
|
||||||
|
+ warnings_helper.check_no_warnings(self),
|
||||||
|
+ ):
|
||||||
|
+ tar.extractall(tempdir / 'outdir')
|
||||||
|
+ self.assertTrue((tempdir / 'outdir/good').exists())
|
||||||
|
+ self.assertEqual((tempdir / 'outdir/ugly').readlink(),
|
||||||
|
+ pathlib.Path('/etc/passwd'))
|
||||||
|
+ self.assertTrue((tempdir / 'bad').exists())
|
||||||
|
+
|
||||||
|
+ def test_rh_default_no_conf(self):
|
||||||
|
+ with self.rh_config_context() as (tempdir, env, tar):
|
||||||
|
+ self.check_rh_default_behavior(tar, tempdir)
|
||||||
|
+
|
||||||
|
+ def test_rh_default_from_file(self):
|
||||||
|
+ lines = ['[tarfile]', 'PYTHON_TARFILE_EXTRACTION_FILTER=fully_trusted']
|
||||||
|
+ with self.rh_config_context(lines) as (tempdir, env, tar):
|
||||||
|
+ self.check_trusted_default(tar, tempdir)
|
||||||
|
+
|
||||||
|
+ def test_rh_empty_config_file(self):
|
||||||
|
+ """Empty config file -> default behavior"""
|
||||||
|
+ lines = []
|
||||||
|
+ with self.rh_config_context(lines) as (tempdir, env, tar):
|
||||||
|
+ self.check_rh_default_behavior(tar, tempdir)
|
||||||
|
+
|
||||||
|
+ def test_empty_config_section(self):
|
||||||
|
+ """Empty section in config file -> default behavior"""
|
||||||
|
+ lines = ['[tarfile]']
|
||||||
|
+ with self.rh_config_context(lines) as (tempdir, env, tar):
|
||||||
|
+ self.check_rh_default_behavior(tar, tempdir)
|
||||||
|
+
|
||||||
|
+ def test_rh_default_empty_config_option(self):
|
||||||
|
+ """Empty option value in config file -> default behavior"""
|
||||||
|
+ lines = ['[tarfile]', 'PYTHON_TARFILE_EXTRACTION_FILTER=']
|
||||||
|
+ with self.rh_config_context(lines) as (tempdir, env, tar):
|
||||||
|
+ self.check_rh_default_behavior(tar, tempdir)
|
||||||
|
+
|
||||||
|
+ def test_bad_config_option(self):
|
||||||
|
+ """Bad option value in config file -> ValueError"""
|
||||||
|
+ lines = ['[tarfile]', 'PYTHON_TARFILE_EXTRACTION_FILTER=unknown!']
|
||||||
|
+ with self.rh_config_context(lines) as (tempdir, env, tar):
|
||||||
|
+ with self.assertRaises(ValueError):
|
||||||
|
+ tar.extractall(tempdir / 'outdir')
|
||||||
|
+
|
||||||
|
+ def test_default_from_envvar(self):
|
||||||
|
+ with self.rh_config_context() as (tempdir, env, tar):
|
||||||
|
+ env['PYTHON_TARFILE_EXTRACTION_FILTER'] = 'fully_trusted'
|
||||||
|
+ self.check_trusted_default(tar, tempdir)
|
||||||
|
+
|
||||||
|
+ def test_empty_envvar(self):
|
||||||
|
+ """Empty env variable -> default behavior"""
|
||||||
|
+ with self.rh_config_context() as (tempdir, env, tar):
|
||||||
|
+ env['PYTHON_TARFILE_EXTRACTION_FILTER'] = ''
|
||||||
|
+ self.check_rh_default_behavior(tar, tempdir)
|
||||||
|
+
|
||||||
|
+ def test_bad_envvar(self):
|
||||||
|
+ with self.rh_config_context() as (tempdir, env, tar):
|
||||||
|
+ env['PYTHON_TARFILE_EXTRACTION_FILTER'] = 'unknown!'
|
||||||
|
+ with self.assertRaises(ValueError):
|
||||||
|
+ tar.extractall(tempdir / 'outdir')
|
||||||
|
+
|
||||||
|
+ def test_envvar_overrides_file(self):
|
||||||
|
+ lines = ['[tarfile]', 'PYTHON_TARFILE_EXTRACTION_FILTER=data']
|
||||||
|
+ with self.rh_config_context(lines) as (tempdir, env, tar):
|
||||||
|
+ env['PYTHON_TARFILE_EXTRACTION_FILTER'] = 'fully_trusted'
|
||||||
|
+ self.check_trusted_default(tar, tempdir)
|
||||||
|
+
|
||||||
|
+ def test_monkeypatch_overrides_envvar(self):
|
||||||
|
+ with self.rh_config_context(None) as (tempdir, env, tar):
|
||||||
|
+ env['PYTHON_TARFILE_EXTRACTION_FILTER'] = 'data'
|
||||||
|
+ with support.swap_attr(
|
||||||
|
+ tarfile.TarFile, 'extraction_filter',
|
||||||
|
+ staticmethod(tarfile.fully_trusted_filter)
|
||||||
|
+ ):
|
||||||
|
+ self.check_trusted_default(tar, tempdir)
|
||||||
|
+
|
||||||
|
+
|
||||||
|
def setUpModule():
|
||||||
|
os_helper.unlink(TEMPDIR)
|
||||||
|
os.makedirs(TEMPDIR)
|
||||||
|
--
|
||||||
|
2.41.0
|
||||||
|
|
@ -1,229 +0,0 @@
|
|||||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
||||||
From: "Miss Islington (bot)"
|
|
||||||
<31488909+miss-islington@users.noreply.github.com>
|
|
||||||
Date: Wed, 17 May 2023 14:41:25 -0700
|
|
||||||
Subject: [PATCH] 00399: CVE-2023-24329
|
|
||||||
|
|
||||||
* gh-102153: Start stripping C0 control and space chars in `urlsplit` (GH-102508)
|
|
||||||
|
|
||||||
`urllib.parse.urlsplit` has already been respecting the WHATWG spec a bit GH-25595.
|
|
||||||
|
|
||||||
This adds more sanitizing to respect the "Remove any leading C0 control or space from input" [rule](https://url.spec.whatwg.org/GH-url-parsing:~:text=Remove%20any%20leading%20and%20trailing%20C0%20control%20or%20space%20from%20input.) in response to [CVE-2023-24329](https://nvd.nist.gov/vuln/detail/CVE-2023-24329).
|
|
||||||
|
|
||||||
---------
|
|
||||||
|
|
||||||
(cherry picked from commit 2f630e1ce18ad2e07428296532a68b11dc66ad10)
|
|
||||||
|
|
||||||
Co-authored-by: Illia Volochii <illia.volochii@gmail.com>
|
|
||||||
Co-authored-by: Gregory P. Smith [Google] <greg@krypto.org>
|
|
||||||
---
|
|
||||||
Doc/library/urllib.parse.rst | 46 +++++++++++++-
|
|
||||||
Lib/test/test_urlparse.py | 61 ++++++++++++++++++-
|
|
||||||
Lib/urllib/parse.py | 12 ++++
|
|
||||||
...-03-07-20-59-17.gh-issue-102153.14CLSZ.rst | 3 +
|
|
||||||
4 files changed, 119 insertions(+), 3 deletions(-)
|
|
||||||
create mode 100644 Misc/NEWS.d/next/Security/2023-03-07-20-59-17.gh-issue-102153.14CLSZ.rst
|
|
||||||
|
|
||||||
diff --git a/Doc/library/urllib.parse.rst b/Doc/library/urllib.parse.rst
|
|
||||||
index 96b3965107..a326e82e30 100644
|
|
||||||
--- a/Doc/library/urllib.parse.rst
|
|
||||||
+++ b/Doc/library/urllib.parse.rst
|
|
||||||
@@ -159,6 +159,10 @@ or on combining URL components into a URL string.
|
|
||||||
ParseResult(scheme='http', netloc='www.cwi.nl:80', path='/%7Eguido/Python.html',
|
|
||||||
params='', query='', fragment='')
|
|
||||||
|
|
||||||
+ .. warning::
|
|
||||||
+
|
|
||||||
+ :func:`urlparse` does not perform validation. See :ref:`URL parsing
|
|
||||||
+ security <url-parsing-security>` for details.
|
|
||||||
|
|
||||||
.. versionchanged:: 3.2
|
|
||||||
Added IPv6 URL parsing capabilities.
|
|
||||||
@@ -324,8 +328,14 @@ or on combining URL components into a URL string.
|
|
||||||
``#``, ``@``, or ``:`` will raise a :exc:`ValueError`. If the URL is
|
|
||||||
decomposed before parsing, no error will be raised.
|
|
||||||
|
|
||||||
- Following the `WHATWG spec`_ that updates RFC 3986, ASCII newline
|
|
||||||
- ``\n``, ``\r`` and tab ``\t`` characters are stripped from the URL.
|
|
||||||
+ Following some of the `WHATWG spec`_ that updates RFC 3986, leading C0
|
|
||||||
+ control and space characters are stripped from the URL. ``\n``,
|
|
||||||
+ ``\r`` and tab ``\t`` characters are removed from the URL at any position.
|
|
||||||
+
|
|
||||||
+ .. warning::
|
|
||||||
+
|
|
||||||
+ :func:`urlsplit` does not perform validation. See :ref:`URL parsing
|
|
||||||
+ security <url-parsing-security>` for details.
|
|
||||||
|
|
||||||
.. versionchanged:: 3.6
|
|
||||||
Out-of-range port numbers now raise :exc:`ValueError`, instead of
|
|
||||||
@@ -338,6 +348,9 @@ or on combining URL components into a URL string.
|
|
||||||
.. versionchanged:: 3.10
|
|
||||||
ASCII newline and tab characters are stripped from the URL.
|
|
||||||
|
|
||||||
+ .. versionchanged:: 3.11.4
|
|
||||||
+ Leading WHATWG C0 control and space characters are stripped from the URL.
|
|
||||||
+
|
|
||||||
.. _WHATWG spec: https://url.spec.whatwg.org/#concept-basic-url-parser
|
|
||||||
|
|
||||||
.. function:: urlunsplit(parts)
|
|
||||||
@@ -414,6 +427,35 @@ or on combining URL components into a URL string.
|
|
||||||
or ``scheme://host/path``). If *url* is not a wrapped URL, it is returned
|
|
||||||
without changes.
|
|
||||||
|
|
||||||
+.. _url-parsing-security:
|
|
||||||
+
|
|
||||||
+URL parsing security
|
|
||||||
+--------------------
|
|
||||||
+
|
|
||||||
+The :func:`urlsplit` and :func:`urlparse` APIs do not perform **validation** of
|
|
||||||
+inputs. They may not raise errors on inputs that other applications consider
|
|
||||||
+invalid. They may also succeed on some inputs that might not be considered
|
|
||||||
+URLs elsewhere. Their purpose is for practical functionality rather than
|
|
||||||
+purity.
|
|
||||||
+
|
|
||||||
+Instead of raising an exception on unusual input, they may instead return some
|
|
||||||
+component parts as empty strings. Or components may contain more than perhaps
|
|
||||||
+they should.
|
|
||||||
+
|
|
||||||
+We recommend that users of these APIs where the values may be used anywhere
|
|
||||||
+with security implications code defensively. Do some verification within your
|
|
||||||
+code before trusting a returned component part. Does that ``scheme`` make
|
|
||||||
+sense? Is that a sensible ``path``? Is there anything strange about that
|
|
||||||
+``hostname``? etc.
|
|
||||||
+
|
|
||||||
+What constitutes a URL is not universally well defined. Different applications
|
|
||||||
+have different needs and desired constraints. For instance the living `WHATWG
|
|
||||||
+spec`_ describes what user facing web clients such as a web browser require.
|
|
||||||
+While :rfc:`3986` is more general. These functions incorporate some aspects of
|
|
||||||
+both, but cannot be claimed compliant with either. The APIs and existing user
|
|
||||||
+code with expectations on specific behaviors predate both standards leading us
|
|
||||||
+to be very cautious about making API behavior changes.
|
|
||||||
+
|
|
||||||
.. _parsing-ascii-encoded-bytes:
|
|
||||||
|
|
||||||
Parsing ASCII Encoded Bytes
|
|
||||||
diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py
|
|
||||||
index b426110723..40f13d631c 100644
|
|
||||||
--- a/Lib/test/test_urlparse.py
|
|
||||||
+++ b/Lib/test/test_urlparse.py
|
|
||||||
@@ -649,6 +649,65 @@ def test_urlsplit_remove_unsafe_bytes(self):
|
|
||||||
self.assertEqual(p.scheme, "http")
|
|
||||||
self.assertEqual(p.geturl(), "http://www.python.org/javascript:alert('msg')/?query=something#fragment")
|
|
||||||
|
|
||||||
+ def test_urlsplit_strip_url(self):
|
|
||||||
+ noise = bytes(range(0, 0x20 + 1))
|
|
||||||
+ base_url = "http://User:Pass@www.python.org:080/doc/?query=yes#frag"
|
|
||||||
+
|
|
||||||
+ url = noise.decode("utf-8") + base_url
|
|
||||||
+ p = urllib.parse.urlsplit(url)
|
|
||||||
+ self.assertEqual(p.scheme, "http")
|
|
||||||
+ self.assertEqual(p.netloc, "User:Pass@www.python.org:080")
|
|
||||||
+ self.assertEqual(p.path, "/doc/")
|
|
||||||
+ self.assertEqual(p.query, "query=yes")
|
|
||||||
+ self.assertEqual(p.fragment, "frag")
|
|
||||||
+ self.assertEqual(p.username, "User")
|
|
||||||
+ self.assertEqual(p.password, "Pass")
|
|
||||||
+ self.assertEqual(p.hostname, "www.python.org")
|
|
||||||
+ self.assertEqual(p.port, 80)
|
|
||||||
+ self.assertEqual(p.geturl(), base_url)
|
|
||||||
+
|
|
||||||
+ url = noise + base_url.encode("utf-8")
|
|
||||||
+ p = urllib.parse.urlsplit(url)
|
|
||||||
+ self.assertEqual(p.scheme, b"http")
|
|
||||||
+ self.assertEqual(p.netloc, b"User:Pass@www.python.org:080")
|
|
||||||
+ self.assertEqual(p.path, b"/doc/")
|
|
||||||
+ self.assertEqual(p.query, b"query=yes")
|
|
||||||
+ self.assertEqual(p.fragment, b"frag")
|
|
||||||
+ self.assertEqual(p.username, b"User")
|
|
||||||
+ self.assertEqual(p.password, b"Pass")
|
|
||||||
+ self.assertEqual(p.hostname, b"www.python.org")
|
|
||||||
+ self.assertEqual(p.port, 80)
|
|
||||||
+ self.assertEqual(p.geturl(), base_url.encode("utf-8"))
|
|
||||||
+
|
|
||||||
+ # Test that trailing space is preserved as some applications rely on
|
|
||||||
+ # this within query strings.
|
|
||||||
+ query_spaces_url = "https://www.python.org:88/doc/?query= "
|
|
||||||
+ p = urllib.parse.urlsplit(noise.decode("utf-8") + query_spaces_url)
|
|
||||||
+ self.assertEqual(p.scheme, "https")
|
|
||||||
+ self.assertEqual(p.netloc, "www.python.org:88")
|
|
||||||
+ self.assertEqual(p.path, "/doc/")
|
|
||||||
+ self.assertEqual(p.query, "query= ")
|
|
||||||
+ self.assertEqual(p.port, 88)
|
|
||||||
+ self.assertEqual(p.geturl(), query_spaces_url)
|
|
||||||
+
|
|
||||||
+ p = urllib.parse.urlsplit("www.pypi.org ")
|
|
||||||
+ # That "hostname" gets considered a "path" due to the
|
|
||||||
+ # trailing space and our existing logic... YUCK...
|
|
||||||
+ # and re-assembles via geturl aka unurlsplit into the original.
|
|
||||||
+ # django.core.validators.URLValidator (at least through v3.2) relies on
|
|
||||||
+ # this, for better or worse, to catch it in a ValidationError via its
|
|
||||||
+ # regular expressions.
|
|
||||||
+ # Here we test the basic round trip concept of such a trailing space.
|
|
||||||
+ self.assertEqual(urllib.parse.urlunsplit(p), "www.pypi.org ")
|
|
||||||
+
|
|
||||||
+ # with scheme as cache-key
|
|
||||||
+ url = "//www.python.org/"
|
|
||||||
+ scheme = noise.decode("utf-8") + "https" + noise.decode("utf-8")
|
|
||||||
+ for _ in range(2):
|
|
||||||
+ p = urllib.parse.urlsplit(url, scheme=scheme)
|
|
||||||
+ self.assertEqual(p.scheme, "https")
|
|
||||||
+ self.assertEqual(p.geturl(), "https://www.python.org/")
|
|
||||||
+
|
|
||||||
def test_attributes_bad_port(self):
|
|
||||||
"""Check handling of invalid ports."""
|
|
||||||
for bytes in (False, True):
|
|
||||||
@@ -656,7 +715,7 @@ def test_attributes_bad_port(self):
|
|
||||||
for port in ("foo", "1.5", "-1", "0x10", "-0", "1_1", " 1", "1 ", "६"):
|
|
||||||
with self.subTest(bytes=bytes, parse=parse, port=port):
|
|
||||||
netloc = "www.example.net:" + port
|
|
||||||
- url = "http://" + netloc
|
|
||||||
+ url = "http://" + netloc + "/"
|
|
||||||
if bytes:
|
|
||||||
if netloc.isascii() and port.isascii():
|
|
||||||
netloc = netloc.encode("ascii")
|
|
||||||
diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py
|
|
||||||
index 69631cbb81..4f06fd509e 100644
|
|
||||||
--- a/Lib/urllib/parse.py
|
|
||||||
+++ b/Lib/urllib/parse.py
|
|
||||||
@@ -25,6 +25,10 @@
|
|
||||||
scenarios for parsing, and for backward compatibility purposes, some
|
|
||||||
parsing quirks from older RFCs are retained. The testcases in
|
|
||||||
test_urlparse.py provides a good indicator of parsing behavior.
|
|
||||||
+
|
|
||||||
+The WHATWG URL Parser spec should also be considered. We are not compliant with
|
|
||||||
+it either due to existing user code API behavior expectations (Hyrum's Law).
|
|
||||||
+It serves as a useful guide when making changes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from collections import namedtuple
|
|
||||||
@@ -79,6 +83,10 @@
|
|
||||||
'0123456789'
|
|
||||||
'+-.')
|
|
||||||
|
|
||||||
+# Leading and trailing C0 control and space to be stripped per WHATWG spec.
|
|
||||||
+# == "".join([chr(i) for i in range(0, 0x20 + 1)])
|
|
||||||
+_WHATWG_C0_CONTROL_OR_SPACE = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f '
|
|
||||||
+
|
|
||||||
# Unsafe bytes to be removed per WHATWG spec
|
|
||||||
_UNSAFE_URL_BYTES_TO_REMOVE = ['\t', '\r', '\n']
|
|
||||||
|
|
||||||
@@ -452,6 +460,10 @@ def urlsplit(url, scheme='', allow_fragments=True):
|
|
||||||
"""
|
|
||||||
|
|
||||||
url, scheme, _coerce_result = _coerce_args(url, scheme)
|
|
||||||
+ # Only lstrip url as some applications rely on preserving trailing space.
|
|
||||||
+ # (https://url.spec.whatwg.org/#concept-basic-url-parser would strip both)
|
|
||||||
+ url = url.lstrip(_WHATWG_C0_CONTROL_OR_SPACE)
|
|
||||||
+ scheme = scheme.strip(_WHATWG_C0_CONTROL_OR_SPACE)
|
|
||||||
|
|
||||||
for b in _UNSAFE_URL_BYTES_TO_REMOVE:
|
|
||||||
url = url.replace(b, "")
|
|
||||||
diff --git a/Misc/NEWS.d/next/Security/2023-03-07-20-59-17.gh-issue-102153.14CLSZ.rst b/Misc/NEWS.d/next/Security/2023-03-07-20-59-17.gh-issue-102153.14CLSZ.rst
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000000..e57ac4ed3a
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/Misc/NEWS.d/next/Security/2023-03-07-20-59-17.gh-issue-102153.14CLSZ.rst
|
|
||||||
@@ -0,0 +1,3 @@
|
|
||||||
+:func:`urllib.parse.urlsplit` now strips leading C0 control and space
|
|
||||||
+characters following the specification for URLs defined by WHATWG in
|
|
||||||
+response to CVE-2023-24329. Patch by Illia Volochii.
|
|
@ -1,608 +0,0 @@
|
|||||||
From 2aeba0764c385241032068b32a5e89a1ec289af7 Mon Sep 17 00:00:00 2001
|
|
||||||
From: =?UTF-8?q?=C5=81ukasz=20Langa?= <lukasz@langa.pl>
|
|
||||||
Date: Tue, 22 Aug 2023 19:53:19 +0200
|
|
||||||
Subject: [PATCH 1/3] gh-108310: Fix CVE-2023-40217: Check for & avoid the ssl
|
|
||||||
pre-close flaw (#108317)
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: text/plain; charset=UTF-8
|
|
||||||
Content-Transfer-Encoding: 8bit
|
|
||||||
|
|
||||||
gh-108310: Fix CVE-2023-40217: Check for & avoid the ssl pre-close flaw
|
|
||||||
|
|
||||||
Instances of `ssl.SSLSocket` were vulnerable to a bypass of the TLS handshake
|
|
||||||
and included protections (like certificate verification) and treating sent
|
|
||||||
unencrypted data as if it were post-handshake TLS encrypted data.
|
|
||||||
|
|
||||||
The vulnerability is caused when a socket is connected, data is sent by the
|
|
||||||
malicious peer and stored in a buffer, and then the malicious peer closes the
|
|
||||||
socket within a small timing window before the other peers’ TLS handshake can
|
|
||||||
begin. After this sequence of events the closed socket will not immediately
|
|
||||||
attempt a TLS handshake due to not being connected but will also allow the
|
|
||||||
buffered data to be read as if a successful TLS handshake had occurred.
|
|
||||||
|
|
||||||
Co-authored-by: Gregory P. Smith [Google LLC] <greg@krypto.org>
|
|
||||||
---
|
|
||||||
Lib/ssl.py | 31 ++-
|
|
||||||
Lib/test/test_ssl.py | 211 ++++++++++++++++++
|
|
||||||
...-08-22-17-39-12.gh-issue-108310.fVM3sg.rst | 7 +
|
|
||||||
3 files changed, 248 insertions(+), 1 deletion(-)
|
|
||||||
create mode 100644 Misc/NEWS.d/next/Security/2023-08-22-17-39-12.gh-issue-108310.fVM3sg.rst
|
|
||||||
|
|
||||||
diff --git a/Lib/ssl.py b/Lib/ssl.py
|
|
||||||
index ebac1d6..ced87d4 100644
|
|
||||||
--- a/Lib/ssl.py
|
|
||||||
+++ b/Lib/ssl.py
|
|
||||||
@@ -1037,7 +1037,7 @@ class SSLSocket(socket):
|
|
||||||
)
|
|
||||||
self = cls.__new__(cls, **kwargs)
|
|
||||||
super(SSLSocket, self).__init__(**kwargs)
|
|
||||||
- self.settimeout(sock.gettimeout())
|
|
||||||
+ sock_timeout = sock.gettimeout()
|
|
||||||
sock.detach()
|
|
||||||
|
|
||||||
self._context = context
|
|
||||||
@@ -1056,9 +1056,38 @@ class SSLSocket(socket):
|
|
||||||
if e.errno != errno.ENOTCONN:
|
|
||||||
raise
|
|
||||||
connected = False
|
|
||||||
+ blocking = self.getblocking()
|
|
||||||
+ self.setblocking(False)
|
|
||||||
+ try:
|
|
||||||
+ # We are not connected so this is not supposed to block, but
|
|
||||||
+ # testing revealed otherwise on macOS and Windows so we do
|
|
||||||
+ # the non-blocking dance regardless. Our raise when any data
|
|
||||||
+ # is found means consuming the data is harmless.
|
|
||||||
+ notconn_pre_handshake_data = self.recv(1)
|
|
||||||
+ except OSError as e:
|
|
||||||
+ # EINVAL occurs for recv(1) on non-connected on unix sockets.
|
|
||||||
+ if e.errno not in (errno.ENOTCONN, errno.EINVAL):
|
|
||||||
+ raise
|
|
||||||
+ notconn_pre_handshake_data = b''
|
|
||||||
+ self.setblocking(blocking)
|
|
||||||
+ if notconn_pre_handshake_data:
|
|
||||||
+ # This prevents pending data sent to the socket before it was
|
|
||||||
+ # closed from escaping to the caller who could otherwise
|
|
||||||
+ # presume it came through a successful TLS connection.
|
|
||||||
+ reason = "Closed before TLS handshake with data in recv buffer."
|
|
||||||
+ notconn_pre_handshake_data_error = SSLError(e.errno, reason)
|
|
||||||
+ # Add the SSLError attributes that _ssl.c always adds.
|
|
||||||
+ notconn_pre_handshake_data_error.reason = reason
|
|
||||||
+ notconn_pre_handshake_data_error.library = None
|
|
||||||
+ try:
|
|
||||||
+ self.close()
|
|
||||||
+ except OSError:
|
|
||||||
+ pass
|
|
||||||
+ raise notconn_pre_handshake_data_error
|
|
||||||
else:
|
|
||||||
connected = True
|
|
||||||
|
|
||||||
+ self.settimeout(sock_timeout) # Must come after setblocking() calls.
|
|
||||||
self._connected = connected
|
|
||||||
if connected:
|
|
||||||
# create the SSL object
|
|
||||||
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
|
|
||||||
index 3b3b869..bc8a098 100644
|
|
||||||
--- a/Lib/test/test_ssl.py
|
|
||||||
+++ b/Lib/test/test_ssl.py
|
|
||||||
@@ -9,11 +9,14 @@ from test.support import os_helper
|
|
||||||
from test.support import socket_helper
|
|
||||||
from test.support import threading_helper
|
|
||||||
from test.support import warnings_helper
|
|
||||||
+import re
|
|
||||||
import socket
|
|
||||||
import select
|
|
||||||
+import struct
|
|
||||||
import time
|
|
||||||
import enum
|
|
||||||
import gc
|
|
||||||
+import http.client
|
|
||||||
import os
|
|
||||||
import errno
|
|
||||||
import pprint
|
|
||||||
@@ -4884,6 +4887,214 @@ class TestSSLDebug(unittest.TestCase):
|
|
||||||
s.connect((HOST, server.port))
|
|
||||||
|
|
||||||
|
|
||||||
+def set_socket_so_linger_on_with_zero_timeout(sock):
|
|
||||||
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+class TestPreHandshakeClose(unittest.TestCase):
|
|
||||||
+ """Verify behavior of close sockets with received data before to the handshake.
|
|
||||||
+ """
|
|
||||||
+
|
|
||||||
+ class SingleConnectionTestServerThread(threading.Thread):
|
|
||||||
+
|
|
||||||
+ def __init__(self, *, name, call_after_accept):
|
|
||||||
+ self.call_after_accept = call_after_accept
|
|
||||||
+ self.received_data = b'' # set by .run()
|
|
||||||
+ self.wrap_error = None # set by .run()
|
|
||||||
+ self.listener = None # set by .start()
|
|
||||||
+ self.port = None # set by .start()
|
|
||||||
+ super().__init__(name=name)
|
|
||||||
+
|
|
||||||
+ def __enter__(self):
|
|
||||||
+ self.start()
|
|
||||||
+ return self
|
|
||||||
+
|
|
||||||
+ def __exit__(self, *args):
|
|
||||||
+ try:
|
|
||||||
+ if self.listener:
|
|
||||||
+ self.listener.close()
|
|
||||||
+ except OSError:
|
|
||||||
+ pass
|
|
||||||
+ self.join()
|
|
||||||
+ self.wrap_error = None # avoid dangling references
|
|
||||||
+
|
|
||||||
+ def start(self):
|
|
||||||
+ self.ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
|
||||||
+ self.ssl_ctx.verify_mode = ssl.CERT_REQUIRED
|
|
||||||
+ self.ssl_ctx.load_verify_locations(cafile=ONLYCERT)
|
|
||||||
+ self.ssl_ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY)
|
|
||||||
+ self.listener = socket.socket()
|
|
||||||
+ self.port = socket_helper.bind_port(self.listener)
|
|
||||||
+ self.listener.settimeout(2.0)
|
|
||||||
+ self.listener.listen(1)
|
|
||||||
+ super().start()
|
|
||||||
+
|
|
||||||
+ def run(self):
|
|
||||||
+ conn, address = self.listener.accept()
|
|
||||||
+ self.listener.close()
|
|
||||||
+ with conn:
|
|
||||||
+ if self.call_after_accept(conn):
|
|
||||||
+ return
|
|
||||||
+ try:
|
|
||||||
+ tls_socket = self.ssl_ctx.wrap_socket(conn, server_side=True)
|
|
||||||
+ except OSError as err: # ssl.SSLError inherits from OSError
|
|
||||||
+ self.wrap_error = err
|
|
||||||
+ else:
|
|
||||||
+ try:
|
|
||||||
+ self.received_data = tls_socket.recv(400)
|
|
||||||
+ except OSError:
|
|
||||||
+ pass # closed, protocol error, etc.
|
|
||||||
+
|
|
||||||
+ def non_linux_skip_if_other_okay_error(self, err):
|
|
||||||
+ if sys.platform == "linux":
|
|
||||||
+ return # Expect the full test setup to always work on Linux.
|
|
||||||
+ if (isinstance(err, ConnectionResetError) or
|
|
||||||
+ (isinstance(err, OSError) and err.errno == errno.EINVAL) or
|
|
||||||
+ re.search('wrong.version.number', getattr(err, "reason", ""), re.I)):
|
|
||||||
+ # On Windows the TCP RST leads to a ConnectionResetError
|
|
||||||
+ # (ECONNRESET) which Linux doesn't appear to surface to userspace.
|
|
||||||
+ # If wrap_socket() winds up on the "if connected:" path and doing
|
|
||||||
+ # the actual wrapping... we get an SSLError from OpenSSL. Typically
|
|
||||||
+ # WRONG_VERSION_NUMBER. While appropriate, neither is the scenario
|
|
||||||
+ # we're specifically trying to test. The way this test is written
|
|
||||||
+ # is known to work on Linux. We'll skip it anywhere else that it
|
|
||||||
+ # does not present as doing so.
|
|
||||||
+ self.skipTest(f"Could not recreate conditions on {sys.platform}:"
|
|
||||||
+ f" {err=}")
|
|
||||||
+ # If maintaining this conditional winds up being a problem.
|
|
||||||
+ # just turn this into an unconditional skip anything but Linux.
|
|
||||||
+ # The important thing is that our CI has the logic covered.
|
|
||||||
+
|
|
||||||
+ def test_preauth_data_to_tls_server(self):
|
|
||||||
+ server_accept_called = threading.Event()
|
|
||||||
+ ready_for_server_wrap_socket = threading.Event()
|
|
||||||
+
|
|
||||||
+ def call_after_accept(unused):
|
|
||||||
+ server_accept_called.set()
|
|
||||||
+ if not ready_for_server_wrap_socket.wait(2.0):
|
|
||||||
+ raise RuntimeError("wrap_socket event never set, test may fail.")
|
|
||||||
+ return False # Tell the server thread to continue.
|
|
||||||
+
|
|
||||||
+ server = self.SingleConnectionTestServerThread(
|
|
||||||
+ call_after_accept=call_after_accept,
|
|
||||||
+ name="preauth_data_to_tls_server")
|
|
||||||
+ self.enterContext(server) # starts it & unittest.TestCase stops it.
|
|
||||||
+
|
|
||||||
+ with socket.socket() as client:
|
|
||||||
+ client.connect(server.listener.getsockname())
|
|
||||||
+ # This forces an immediate connection close via RST on .close().
|
|
||||||
+ set_socket_so_linger_on_with_zero_timeout(client)
|
|
||||||
+ client.setblocking(False)
|
|
||||||
+
|
|
||||||
+ server_accept_called.wait()
|
|
||||||
+ client.send(b"DELETE /data HTTP/1.0\r\n\r\n")
|
|
||||||
+ client.close() # RST
|
|
||||||
+
|
|
||||||
+ ready_for_server_wrap_socket.set()
|
|
||||||
+ server.join()
|
|
||||||
+ wrap_error = server.wrap_error
|
|
||||||
+ self.assertEqual(b"", server.received_data)
|
|
||||||
+ self.assertIsInstance(wrap_error, OSError) # All platforms.
|
|
||||||
+ self.non_linux_skip_if_other_okay_error(wrap_error)
|
|
||||||
+ self.assertIsInstance(wrap_error, ssl.SSLError)
|
|
||||||
+ self.assertIn("before TLS handshake with data", wrap_error.args[1])
|
|
||||||
+ self.assertIn("before TLS handshake with data", wrap_error.reason)
|
|
||||||
+ self.assertNotEqual(0, wrap_error.args[0])
|
|
||||||
+ self.assertIsNone(wrap_error.library, msg="attr must exist")
|
|
||||||
+
|
|
||||||
+ def test_preauth_data_to_tls_client(self):
|
|
||||||
+ client_can_continue_with_wrap_socket = threading.Event()
|
|
||||||
+
|
|
||||||
+ def call_after_accept(conn_to_client):
|
|
||||||
+ # This forces an immediate connection close via RST on .close().
|
|
||||||
+ set_socket_so_linger_on_with_zero_timeout(conn_to_client)
|
|
||||||
+ conn_to_client.send(
|
|
||||||
+ b"HTTP/1.0 307 Temporary Redirect\r\n"
|
|
||||||
+ b"Location: https://example.com/someone-elses-server\r\n"
|
|
||||||
+ b"\r\n")
|
|
||||||
+ conn_to_client.close() # RST
|
|
||||||
+ client_can_continue_with_wrap_socket.set()
|
|
||||||
+ return True # Tell the server to stop.
|
|
||||||
+
|
|
||||||
+ server = self.SingleConnectionTestServerThread(
|
|
||||||
+ call_after_accept=call_after_accept,
|
|
||||||
+ name="preauth_data_to_tls_client")
|
|
||||||
+ self.enterContext(server) # starts it & unittest.TestCase stops it.
|
|
||||||
+ # Redundant; call_after_accept sets SO_LINGER on the accepted conn.
|
|
||||||
+ set_socket_so_linger_on_with_zero_timeout(server.listener)
|
|
||||||
+
|
|
||||||
+ with socket.socket() as client:
|
|
||||||
+ client.connect(server.listener.getsockname())
|
|
||||||
+ if not client_can_continue_with_wrap_socket.wait(2.0):
|
|
||||||
+ self.fail("test server took too long.")
|
|
||||||
+ ssl_ctx = ssl.create_default_context()
|
|
||||||
+ try:
|
|
||||||
+ tls_client = ssl_ctx.wrap_socket(
|
|
||||||
+ client, server_hostname="localhost")
|
|
||||||
+ except OSError as err: # SSLError inherits from OSError
|
|
||||||
+ wrap_error = err
|
|
||||||
+ received_data = b""
|
|
||||||
+ else:
|
|
||||||
+ wrap_error = None
|
|
||||||
+ received_data = tls_client.recv(400)
|
|
||||||
+ tls_client.close()
|
|
||||||
+
|
|
||||||
+ server.join()
|
|
||||||
+ self.assertEqual(b"", received_data)
|
|
||||||
+ self.assertIsInstance(wrap_error, OSError) # All platforms.
|
|
||||||
+ self.non_linux_skip_if_other_okay_error(wrap_error)
|
|
||||||
+ self.assertIsInstance(wrap_error, ssl.SSLError)
|
|
||||||
+ self.assertIn("before TLS handshake with data", wrap_error.args[1])
|
|
||||||
+ self.assertIn("before TLS handshake with data", wrap_error.reason)
|
|
||||||
+ self.assertNotEqual(0, wrap_error.args[0])
|
|
||||||
+ self.assertIsNone(wrap_error.library, msg="attr must exist")
|
|
||||||
+
|
|
||||||
+ def test_https_client_non_tls_response_ignored(self):
|
|
||||||
+
|
|
||||||
+ server_responding = threading.Event()
|
|
||||||
+
|
|
||||||
+ class SynchronizedHTTPSConnection(http.client.HTTPSConnection):
|
|
||||||
+ def connect(self):
|
|
||||||
+ http.client.HTTPConnection.connect(self)
|
|
||||||
+ # Wait for our fault injection server to have done its thing.
|
|
||||||
+ if not server_responding.wait(1.0) and support.verbose:
|
|
||||||
+ sys.stdout.write("server_responding event never set.")
|
|
||||||
+ self.sock = self._context.wrap_socket(
|
|
||||||
+ self.sock, server_hostname=self.host)
|
|
||||||
+
|
|
||||||
+ def call_after_accept(conn_to_client):
|
|
||||||
+ # This forces an immediate connection close via RST on .close().
|
|
||||||
+ set_socket_so_linger_on_with_zero_timeout(conn_to_client)
|
|
||||||
+ conn_to_client.send(
|
|
||||||
+ b"HTTP/1.0 402 Payment Required\r\n"
|
|
||||||
+ b"\r\n")
|
|
||||||
+ conn_to_client.close() # RST
|
|
||||||
+ server_responding.set()
|
|
||||||
+ return True # Tell the server to stop.
|
|
||||||
+
|
|
||||||
+ server = self.SingleConnectionTestServerThread(
|
|
||||||
+ call_after_accept=call_after_accept,
|
|
||||||
+ name="non_tls_http_RST_responder")
|
|
||||||
+ self.enterContext(server) # starts it & unittest.TestCase stops it.
|
|
||||||
+ # Redundant; call_after_accept sets SO_LINGER on the accepted conn.
|
|
||||||
+ set_socket_so_linger_on_with_zero_timeout(server.listener)
|
|
||||||
+
|
|
||||||
+ connection = SynchronizedHTTPSConnection(
|
|
||||||
+ f"localhost",
|
|
||||||
+ port=server.port,
|
|
||||||
+ context=ssl.create_default_context(),
|
|
||||||
+ timeout=2.0,
|
|
||||||
+ )
|
|
||||||
+ # There are lots of reasons this raises as desired, long before this
|
|
||||||
+ # test was added. Sending the request requires a successful TLS wrapped
|
|
||||||
+ # socket; that fails if the connection is broken. It may seem pointless
|
|
||||||
+ # to test this. It serves as an illustration of something that we never
|
|
||||||
+ # want to happen... properly not happening.
|
|
||||||
+ with self.assertRaises(OSError) as err_ctx:
|
|
||||||
+ connection.request("HEAD", "/test", headers={"Host": "localhost"})
|
|
||||||
+ response = connection.getresponse()
|
|
||||||
+
|
|
||||||
+
|
|
||||||
class TestEnumerations(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_tlsversion(self):
|
|
||||||
diff --git a/Misc/NEWS.d/next/Security/2023-08-22-17-39-12.gh-issue-108310.fVM3sg.rst b/Misc/NEWS.d/next/Security/2023-08-22-17-39-12.gh-issue-108310.fVM3sg.rst
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000..403c77a
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/Misc/NEWS.d/next/Security/2023-08-22-17-39-12.gh-issue-108310.fVM3sg.rst
|
|
||||||
@@ -0,0 +1,7 @@
|
|
||||||
+Fixed an issue where instances of :class:`ssl.SSLSocket` were vulnerable to
|
|
||||||
+a bypass of the TLS handshake and included protections (like certificate
|
|
||||||
+verification) and treating sent unencrypted data as if it were
|
|
||||||
+post-handshake TLS encrypted data. Security issue reported as
|
|
||||||
+`CVE-2023-40217
|
|
||||||
+<https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-40217>`_ by
|
|
||||||
+Aapo Oksman. Patch by Gregory P. Smith.
|
|
||||||
--
|
|
||||||
2.41.0
|
|
||||||
|
|
||||||
|
|
||||||
From 020da41ffe4f98efe85131e943dcf7b75a5a5f3a Mon Sep 17 00:00:00 2001
|
|
||||||
From: "Miss Islington (bot)"
|
|
||||||
<31488909+miss-islington@users.noreply.github.com>
|
|
||||||
Date: Wed, 23 Aug 2023 03:10:04 -0700
|
|
||||||
Subject: [PATCH 2/3] gh-108342: Break ref cycle in SSLSocket._create() exc
|
|
||||||
(GH-108344) (#108349)
|
|
||||||
|
|
||||||
Explicitly break a reference cycle when SSLSocket._create() raises an
|
|
||||||
exception. Clear the variable storing the exception, since the
|
|
||||||
exception traceback contains the variables and so creates a reference
|
|
||||||
cycle.
|
|
||||||
|
|
||||||
This test leak was introduced by the test added for the fix of GH-108310.
|
|
||||||
(cherry picked from commit 64f99350351bc46e016b2286f36ba7cd669b79e3)
|
|
||||||
|
|
||||||
Co-authored-by: Victor Stinner <vstinner@python.org>
|
|
||||||
---
|
|
||||||
Lib/ssl.py | 6 +++++-
|
|
||||||
1 file changed, 5 insertions(+), 1 deletion(-)
|
|
||||||
|
|
||||||
diff --git a/Lib/ssl.py b/Lib/ssl.py
|
|
||||||
index ced87d4..48d229f 100644
|
|
||||||
--- a/Lib/ssl.py
|
|
||||||
+++ b/Lib/ssl.py
|
|
||||||
@@ -1083,7 +1083,11 @@ class SSLSocket(socket):
|
|
||||||
self.close()
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
- raise notconn_pre_handshake_data_error
|
|
||||||
+ try:
|
|
||||||
+ raise notconn_pre_handshake_data_error
|
|
||||||
+ finally:
|
|
||||||
+ # Explicitly break the reference cycle.
|
|
||||||
+ notconn_pre_handshake_data_error = None
|
|
||||||
else:
|
|
||||||
connected = True
|
|
||||||
|
|
||||||
--
|
|
||||||
2.41.0
|
|
||||||
|
|
||||||
|
|
||||||
From e20339d85a893c7915b747f7bd80cc5c6fcc51c1 Mon Sep 17 00:00:00 2001
|
|
||||||
From: =?UTF-8?q?=C5=81ukasz=20Langa?= <lukasz@langa.pl>
|
|
||||||
Date: Thu, 24 Aug 2023 12:08:52 +0200
|
|
||||||
Subject: [PATCH 3/3] gh-108342: Make ssl TestPreHandshakeClose more reliable
|
|
||||||
(GH-108370) (#108405)
|
|
||||||
|
|
||||||
* In preauth tests of test_ssl, explicitly break reference cycles
|
|
||||||
invoving SingleConnectionTestServerThread to make sure that the
|
|
||||||
thread is deleted. Otherwise, the test marks the environment as
|
|
||||||
altered because the threading module sees a "dangling thread"
|
|
||||||
(SingleConnectionTestServerThread). This test leak was introduced
|
|
||||||
by the test added for the fix of issue gh-108310.
|
|
||||||
* Use support.SHORT_TIMEOUT instead of hardcoded 1.0 or 2.0 seconds
|
|
||||||
timeout.
|
|
||||||
* SingleConnectionTestServerThread.run() catchs TimeoutError
|
|
||||||
* Fix a race condition (missing synchronization) in
|
|
||||||
test_preauth_data_to_tls_client(): the server now waits until the
|
|
||||||
client connect() completed in call_after_accept().
|
|
||||||
* test_https_client_non_tls_response_ignored() calls server.join()
|
|
||||||
explicitly.
|
|
||||||
* Replace "localhost" with server.listener.getsockname()[0].
|
|
||||||
(cherry picked from commit 592bacb6fc0833336c0453e818e9b95016e9fd47)
|
|
||||||
|
|
||||||
Co-authored-by: Victor Stinner <vstinner@python.org>
|
|
||||||
---
|
|
||||||
Lib/test/test_ssl.py | 102 ++++++++++++++++++++++++++++++-------------
|
|
||||||
1 file changed, 71 insertions(+), 31 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
|
|
||||||
index bc8a098..f1633ee 100644
|
|
||||||
--- a/Lib/test/test_ssl.py
|
|
||||||
+++ b/Lib/test/test_ssl.py
|
|
||||||
@@ -4897,12 +4897,16 @@ class TestPreHandshakeClose(unittest.TestCase):
|
|
||||||
|
|
||||||
class SingleConnectionTestServerThread(threading.Thread):
|
|
||||||
|
|
||||||
- def __init__(self, *, name, call_after_accept):
|
|
||||||
+ def __init__(self, *, name, call_after_accept, timeout=None):
|
|
||||||
self.call_after_accept = call_after_accept
|
|
||||||
self.received_data = b'' # set by .run()
|
|
||||||
self.wrap_error = None # set by .run()
|
|
||||||
self.listener = None # set by .start()
|
|
||||||
self.port = None # set by .start()
|
|
||||||
+ if timeout is None:
|
|
||||||
+ self.timeout = support.SHORT_TIMEOUT
|
|
||||||
+ else:
|
|
||||||
+ self.timeout = timeout
|
|
||||||
super().__init__(name=name)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
@@ -4925,13 +4929,19 @@ class TestPreHandshakeClose(unittest.TestCase):
|
|
||||||
self.ssl_ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY)
|
|
||||||
self.listener = socket.socket()
|
|
||||||
self.port = socket_helper.bind_port(self.listener)
|
|
||||||
- self.listener.settimeout(2.0)
|
|
||||||
+ self.listener.settimeout(self.timeout)
|
|
||||||
self.listener.listen(1)
|
|
||||||
super().start()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
- conn, address = self.listener.accept()
|
|
||||||
- self.listener.close()
|
|
||||||
+ try:
|
|
||||||
+ conn, address = self.listener.accept()
|
|
||||||
+ except TimeoutError:
|
|
||||||
+ # on timeout, just close the listener
|
|
||||||
+ return
|
|
||||||
+ finally:
|
|
||||||
+ self.listener.close()
|
|
||||||
+
|
|
||||||
with conn:
|
|
||||||
if self.call_after_accept(conn):
|
|
||||||
return
|
|
||||||
@@ -4959,8 +4969,13 @@ class TestPreHandshakeClose(unittest.TestCase):
|
|
||||||
# we're specifically trying to test. The way this test is written
|
|
||||||
# is known to work on Linux. We'll skip it anywhere else that it
|
|
||||||
# does not present as doing so.
|
|
||||||
- self.skipTest(f"Could not recreate conditions on {sys.platform}:"
|
|
||||||
- f" {err=}")
|
|
||||||
+ try:
|
|
||||||
+ self.skipTest(f"Could not recreate conditions on {sys.platform}:"
|
|
||||||
+ f" {err=}")
|
|
||||||
+ finally:
|
|
||||||
+ # gh-108342: Explicitly break the reference cycle
|
|
||||||
+ err = None
|
|
||||||
+
|
|
||||||
# If maintaining this conditional winds up being a problem.
|
|
||||||
# just turn this into an unconditional skip anything but Linux.
|
|
||||||
# The important thing is that our CI has the logic covered.
|
|
||||||
@@ -4971,7 +4986,7 @@ class TestPreHandshakeClose(unittest.TestCase):
|
|
||||||
|
|
||||||
def call_after_accept(unused):
|
|
||||||
server_accept_called.set()
|
|
||||||
- if not ready_for_server_wrap_socket.wait(2.0):
|
|
||||||
+ if not ready_for_server_wrap_socket.wait(support.SHORT_TIMEOUT):
|
|
||||||
raise RuntimeError("wrap_socket event never set, test may fail.")
|
|
||||||
return False # Tell the server thread to continue.
|
|
||||||
|
|
||||||
@@ -4992,20 +5007,31 @@ class TestPreHandshakeClose(unittest.TestCase):
|
|
||||||
|
|
||||||
ready_for_server_wrap_socket.set()
|
|
||||||
server.join()
|
|
||||||
+
|
|
||||||
wrap_error = server.wrap_error
|
|
||||||
- self.assertEqual(b"", server.received_data)
|
|
||||||
- self.assertIsInstance(wrap_error, OSError) # All platforms.
|
|
||||||
- self.non_linux_skip_if_other_okay_error(wrap_error)
|
|
||||||
- self.assertIsInstance(wrap_error, ssl.SSLError)
|
|
||||||
- self.assertIn("before TLS handshake with data", wrap_error.args[1])
|
|
||||||
- self.assertIn("before TLS handshake with data", wrap_error.reason)
|
|
||||||
- self.assertNotEqual(0, wrap_error.args[0])
|
|
||||||
- self.assertIsNone(wrap_error.library, msg="attr must exist")
|
|
||||||
+ server.wrap_error = None
|
|
||||||
+ try:
|
|
||||||
+ self.assertEqual(b"", server.received_data)
|
|
||||||
+ self.assertIsInstance(wrap_error, OSError) # All platforms.
|
|
||||||
+ self.non_linux_skip_if_other_okay_error(wrap_error)
|
|
||||||
+ self.assertIsInstance(wrap_error, ssl.SSLError)
|
|
||||||
+ self.assertIn("before TLS handshake with data", wrap_error.args[1])
|
|
||||||
+ self.assertIn("before TLS handshake with data", wrap_error.reason)
|
|
||||||
+ self.assertNotEqual(0, wrap_error.args[0])
|
|
||||||
+ self.assertIsNone(wrap_error.library, msg="attr must exist")
|
|
||||||
+ finally:
|
|
||||||
+ # gh-108342: Explicitly break the reference cycle
|
|
||||||
+ wrap_error = None
|
|
||||||
+ server = None
|
|
||||||
|
|
||||||
def test_preauth_data_to_tls_client(self):
|
|
||||||
+ server_can_continue_with_wrap_socket = threading.Event()
|
|
||||||
client_can_continue_with_wrap_socket = threading.Event()
|
|
||||||
|
|
||||||
def call_after_accept(conn_to_client):
|
|
||||||
+ if not server_can_continue_with_wrap_socket.wait(support.SHORT_TIMEOUT):
|
|
||||||
+ print("ERROR: test client took too long")
|
|
||||||
+
|
|
||||||
# This forces an immediate connection close via RST on .close().
|
|
||||||
set_socket_so_linger_on_with_zero_timeout(conn_to_client)
|
|
||||||
conn_to_client.send(
|
|
||||||
@@ -5025,8 +5051,10 @@ class TestPreHandshakeClose(unittest.TestCase):
|
|
||||||
|
|
||||||
with socket.socket() as client:
|
|
||||||
client.connect(server.listener.getsockname())
|
|
||||||
- if not client_can_continue_with_wrap_socket.wait(2.0):
|
|
||||||
- self.fail("test server took too long.")
|
|
||||||
+ server_can_continue_with_wrap_socket.set()
|
|
||||||
+
|
|
||||||
+ if not client_can_continue_with_wrap_socket.wait(support.SHORT_TIMEOUT):
|
|
||||||
+ self.fail("test server took too long")
|
|
||||||
ssl_ctx = ssl.create_default_context()
|
|
||||||
try:
|
|
||||||
tls_client = ssl_ctx.wrap_socket(
|
|
||||||
@@ -5040,24 +5068,31 @@ class TestPreHandshakeClose(unittest.TestCase):
|
|
||||||
tls_client.close()
|
|
||||||
|
|
||||||
server.join()
|
|
||||||
- self.assertEqual(b"", received_data)
|
|
||||||
- self.assertIsInstance(wrap_error, OSError) # All platforms.
|
|
||||||
- self.non_linux_skip_if_other_okay_error(wrap_error)
|
|
||||||
- self.assertIsInstance(wrap_error, ssl.SSLError)
|
|
||||||
- self.assertIn("before TLS handshake with data", wrap_error.args[1])
|
|
||||||
- self.assertIn("before TLS handshake with data", wrap_error.reason)
|
|
||||||
- self.assertNotEqual(0, wrap_error.args[0])
|
|
||||||
- self.assertIsNone(wrap_error.library, msg="attr must exist")
|
|
||||||
+ try:
|
|
||||||
+ self.assertEqual(b"", received_data)
|
|
||||||
+ self.assertIsInstance(wrap_error, OSError) # All platforms.
|
|
||||||
+ self.non_linux_skip_if_other_okay_error(wrap_error)
|
|
||||||
+ self.assertIsInstance(wrap_error, ssl.SSLError)
|
|
||||||
+ self.assertIn("before TLS handshake with data", wrap_error.args[1])
|
|
||||||
+ self.assertIn("before TLS handshake with data", wrap_error.reason)
|
|
||||||
+ self.assertNotEqual(0, wrap_error.args[0])
|
|
||||||
+ self.assertIsNone(wrap_error.library, msg="attr must exist")
|
|
||||||
+ finally:
|
|
||||||
+ # gh-108342: Explicitly break the reference cycle
|
|
||||||
+ wrap_error = None
|
|
||||||
+ server = None
|
|
||||||
|
|
||||||
def test_https_client_non_tls_response_ignored(self):
|
|
||||||
-
|
|
||||||
server_responding = threading.Event()
|
|
||||||
|
|
||||||
class SynchronizedHTTPSConnection(http.client.HTTPSConnection):
|
|
||||||
def connect(self):
|
|
||||||
+ # Call clear text HTTP connect(), not the encrypted HTTPS (TLS)
|
|
||||||
+ # connect(): wrap_socket() is called manually below.
|
|
||||||
http.client.HTTPConnection.connect(self)
|
|
||||||
+
|
|
||||||
# Wait for our fault injection server to have done its thing.
|
|
||||||
- if not server_responding.wait(1.0) and support.verbose:
|
|
||||||
+ if not server_responding.wait(support.SHORT_TIMEOUT) and support.verbose:
|
|
||||||
sys.stdout.write("server_responding event never set.")
|
|
||||||
self.sock = self._context.wrap_socket(
|
|
||||||
self.sock, server_hostname=self.host)
|
|
||||||
@@ -5072,28 +5107,33 @@ class TestPreHandshakeClose(unittest.TestCase):
|
|
||||||
server_responding.set()
|
|
||||||
return True # Tell the server to stop.
|
|
||||||
|
|
||||||
+ timeout = 2.0
|
|
||||||
server = self.SingleConnectionTestServerThread(
|
|
||||||
call_after_accept=call_after_accept,
|
|
||||||
- name="non_tls_http_RST_responder")
|
|
||||||
+ name="non_tls_http_RST_responder",
|
|
||||||
+ timeout=timeout)
|
|
||||||
self.enterContext(server) # starts it & unittest.TestCase stops it.
|
|
||||||
# Redundant; call_after_accept sets SO_LINGER on the accepted conn.
|
|
||||||
set_socket_so_linger_on_with_zero_timeout(server.listener)
|
|
||||||
|
|
||||||
connection = SynchronizedHTTPSConnection(
|
|
||||||
- f"localhost",
|
|
||||||
+ server.listener.getsockname()[0],
|
|
||||||
port=server.port,
|
|
||||||
context=ssl.create_default_context(),
|
|
||||||
- timeout=2.0,
|
|
||||||
+ timeout=timeout,
|
|
||||||
)
|
|
||||||
+
|
|
||||||
# There are lots of reasons this raises as desired, long before this
|
|
||||||
# test was added. Sending the request requires a successful TLS wrapped
|
|
||||||
# socket; that fails if the connection is broken. It may seem pointless
|
|
||||||
# to test this. It serves as an illustration of something that we never
|
|
||||||
# want to happen... properly not happening.
|
|
||||||
- with self.assertRaises(OSError) as err_ctx:
|
|
||||||
+ with self.assertRaises(OSError):
|
|
||||||
connection.request("HEAD", "/test", headers={"Host": "localhost"})
|
|
||||||
response = connection.getresponse()
|
|
||||||
|
|
||||||
+ server.join()
|
|
||||||
+
|
|
||||||
|
|
||||||
class TestEnumerations(unittest.TestCase):
|
|
||||||
|
|
||||||
--
|
|
||||||
2.41.0
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
|||||||
-----BEGIN PGP SIGNATURE-----
|
|
||||||
|
|
||||||
iQIzBAABCAAdFiEEz9yiRbEEPPKl+Xhl/+h0BBaL2EcFAmPiV84ACgkQ/+h0BBaL
|
|
||||||
2EeZ1xAAwBi0AEjUlZ9oeC54VuqC/XLuVwc3xWf+Irw/5mJA2/weJHoQqG9aEDkB
|
|
||||||
ph1pDJ6G/vDyKdjh8NZKkKftIL9pggRpAcA4mQ3XcDMKI/J+EQe5P/BwsTGClLhK
|
|
||||||
cZg6IcQKZvo9djfyRz48w9wfKs34NasBgoFQP+hOzmU10UMrcR7gUSB2ZgMVMDID
|
|
||||||
0rK1w2aPmZmDLUltBhf6Xb2voUYo+3jINLHWmQC6tdDOBxtxv222dhxS1mvpV7Zu
|
|
||||||
Xw8do9OsQxonc+owkpciMKDLcFoVmkdQPz9bmvHJKovMXT2RY7FEam9H7ukr35fC
|
|
||||||
xA6BKnyMgvWIWQVTwjBhcz3C85adzAz/ypHNTbJOuPxp1ZP8qO3D6vPlhZIFyTeJ
|
|
||||||
7LhagUBUkIKKtbz7u3ERJgvA6tn3UVyLOXM1DnaKkXQ1FgSymgWPRU7BsxanQ8FD
|
|
||||||
QkfTjC8fatZLCewNfGInkeAdLue+rMwZc8Q6vw2CAmcVdOKsQ98Db/FLF5sC+Kjz
|
|
||||||
D3brUESEX1ELcVk7vumUI0/z+MECF11dpv5hPOZ4cZDoInsNu846TfU0rzOeVe7H
|
|
||||||
gGO6Ae/Lu5gG09TNqepbFGA/dWR8V3zdLs5ZShTT4FsNFrHh7GDAEAMZSwT3AsVZ
|
|
||||||
TjOdU3+xEGsEfrYWRXOkhVIQdJtuovwv9+me5YWeyC4Puzp0Zwk=
|
|
||||||
=8/cW
|
|
||||||
-----END PGP SIGNATURE-----
|
|
16
SOURCES/Python-3.11.5.tar.xz.asc
Normal file
16
SOURCES/Python-3.11.5.tar.xz.asc
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQIzBAABCAAdFiEEz9yiRbEEPPKl+Xhl/+h0BBaL2EcFAmTnS9sACgkQ/+h0BBaL
|
||||||
|
2EeG8g//Q6EC79SSFl4BPb064d8X1q8agfLN+D07N6ULsaOL1baOClLbMxiCgquQ
|
||||||
|
R1CVzEXc0osL25Xw/7rTIBO0tCSS2yNcQ3GMuetBO4wfofDvs9V2ydaVQdrIHEQm
|
||||||
|
OTOveioF9TOaQ/zozi9Hecl4RY289kCD64sWNkwPYBJzO9KQD/UGRS/b5a4CGKyP
|
||||||
|
GSQEFdfevYsuLxLtwNh1z8af1LKRGhuWoZOBhDgpz4foH4EQdz80sssXzm2vG3tS
|
||||||
|
hAeniPphjZyRfl8kC1C86M/hH08S3h4bf/LF/OQ0OYUrwOquqOsLlz03XzJ+COGK
|
||||||
|
nBa/CGsFrxeby2oI/XF8YZrFzt9LKyWYc2p+AIU+u2EnYwOmAkrE4QaczqOV8ldD
|
||||||
|
UvfZLTeMVG/Q6JGkNS/OyM3SZoVKDdGJlg5yVAQtbQjdsB5QjVDcysLhhZ+qnuJv
|
||||||
|
pnQ6anbbX5r4X4ji/2Uar5cwO/jf7QenTKLtgGY67Q2oRE20w6F5rbYHEdO4a4MM
|
||||||
|
OkI/0pUaU5MGRJfowwtcD5AbWPKo1XXqw2UY8p+biEaVQOj+kWhoB8YA5Qz1utHJ
|
||||||
|
GiPP69oDIjfn3sPMxB/C1pBdB/m3i8za58b+G3aYtAWWP1q0abaHqPusACotvxPp
|
||||||
|
3IvB3ryLlTyUYqqTiDp9wgYh2Nr+a9b6i6yW0ptcdycnzDWC1/E=
|
||||||
|
=Lzjg
|
||||||
|
-----END PGP SIGNATURE-----
|
@ -16,6 +16,7 @@ LEVELS = (None, 1, 2)
|
|||||||
# list of globs of test and other files that we expect not to have bytecode
|
# list of globs of test and other files that we expect not to have bytecode
|
||||||
not_compiled = [
|
not_compiled = [
|
||||||
'/usr/bin/*',
|
'/usr/bin/*',
|
||||||
|
'/usr/lib/rpm/redhat/*',
|
||||||
'*/test/bad_coding.py',
|
'*/test/bad_coding.py',
|
||||||
'*/test/bad_coding2.py',
|
'*/test/bad_coding2.py',
|
||||||
'*/test/badsyntax_*.py',
|
'*/test/badsyntax_*.py',
|
||||||
|
171
SOURCES/import_all_modules_py3_11.py
Normal file
171
SOURCES/import_all_modules_py3_11.py
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
'''Script to perform import of each module given to %%py_check_import
|
||||||
|
'''
|
||||||
|
import argparse
|
||||||
|
import importlib
|
||||||
|
import fnmatch
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import site
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def read_modules_files(file_paths):
|
||||||
|
'''Read module names from the files (modules must be newline separated).
|
||||||
|
|
||||||
|
Return the module names list or, if no files were provided, an empty list.
|
||||||
|
'''
|
||||||
|
|
||||||
|
if not file_paths:
|
||||||
|
return []
|
||||||
|
|
||||||
|
modules = []
|
||||||
|
for file in file_paths:
|
||||||
|
file_contents = file.read_text()
|
||||||
|
modules.extend(file_contents.split())
|
||||||
|
return modules
|
||||||
|
|
||||||
|
|
||||||
|
def read_modules_from_cli(argv):
|
||||||
|
'''Read module names from command-line arguments (space or comma separated).
|
||||||
|
|
||||||
|
Return the module names list.
|
||||||
|
'''
|
||||||
|
|
||||||
|
if not argv:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# %%py3_check_import allows to separate module list with comma or whitespace,
|
||||||
|
# we need to unify the output to a list of particular elements
|
||||||
|
modules_as_str = ' '.join(argv)
|
||||||
|
modules = re.split(r'[\s,]+', modules_as_str)
|
||||||
|
# Because of shell expansion in some less typical cases it may happen
|
||||||
|
# that a trailing space will occur at the end of the list.
|
||||||
|
# Remove the empty items from the list before passing it further
|
||||||
|
modules = [m for m in modules if m]
|
||||||
|
return modules
|
||||||
|
|
||||||
|
|
||||||
|
def filter_top_level_modules_only(modules):
|
||||||
|
'''Filter out entries with nested modules (containing dot) ie. 'foo.bar'.
|
||||||
|
|
||||||
|
Return the list of top-level modules.
|
||||||
|
'''
|
||||||
|
|
||||||
|
return [module for module in modules if '.' not in module]
|
||||||
|
|
||||||
|
|
||||||
|
def any_match(text, globs):
|
||||||
|
'''Return True if any of given globs fnmatchcase's the given text.'''
|
||||||
|
|
||||||
|
return any(fnmatch.fnmatchcase(text, g) for g in globs)
|
||||||
|
|
||||||
|
|
||||||
|
def exclude_unwanted_module_globs(globs, modules):
|
||||||
|
'''Filter out entries which match the either of the globs given as argv.
|
||||||
|
|
||||||
|
Return the list of filtered modules.
|
||||||
|
'''
|
||||||
|
|
||||||
|
return [m for m in modules if not any_match(m, globs)]
|
||||||
|
|
||||||
|
|
||||||
|
def read_modules_from_all_args(args):
|
||||||
|
'''Return a joined list of modules from all given command-line arguments.
|
||||||
|
'''
|
||||||
|
|
||||||
|
modules = read_modules_files(args.filename)
|
||||||
|
modules.extend(read_modules_from_cli(args.modules))
|
||||||
|
if args.exclude:
|
||||||
|
modules = exclude_unwanted_module_globs(args.exclude, modules)
|
||||||
|
|
||||||
|
if args.top_level:
|
||||||
|
modules = filter_top_level_modules_only(modules)
|
||||||
|
|
||||||
|
# Error when someone accidentally managed to filter out everything
|
||||||
|
if len(modules) == 0:
|
||||||
|
raise ValueError('No modules to check were left')
|
||||||
|
|
||||||
|
return modules
|
||||||
|
|
||||||
|
|
||||||
|
def import_modules(modules):
|
||||||
|
'''Procedure to perform import check for each module name from the given list of modules.
|
||||||
|
'''
|
||||||
|
|
||||||
|
for module in modules:
|
||||||
|
print('Check import:', module, file=sys.stderr)
|
||||||
|
importlib.import_module(module)
|
||||||
|
|
||||||
|
|
||||||
|
def argparser():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Generate list of all importable modules for import check.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'modules', nargs='*',
|
||||||
|
help=('Add modules to check the import (space or comma separated).'),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-f', '--filename', action='append', type=Path,
|
||||||
|
help='Add importable module names list from file.',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-t', '--top-level', action='store_true',
|
||||||
|
help='Check only top-level modules.',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-e', '--exclude', action='append',
|
||||||
|
help='Provide modules globs to be excluded from the check.',
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def remove_unwanteds_from_sys_path():
|
||||||
|
'''Remove cwd and this script's parent from sys.path for the import test.
|
||||||
|
Bring the original contents back after import is done (or failed)
|
||||||
|
'''
|
||||||
|
|
||||||
|
cwd_absolute = Path.cwd().absolute()
|
||||||
|
this_file_parent = Path(__file__).parent.absolute()
|
||||||
|
old_sys_path = list(sys.path)
|
||||||
|
for path in old_sys_path:
|
||||||
|
if Path(path).absolute() in (cwd_absolute, this_file_parent):
|
||||||
|
sys.path.remove(path)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
sys.path = old_sys_path
|
||||||
|
|
||||||
|
|
||||||
|
def addsitedirs_from_environ():
|
||||||
|
'''Load directories from the _PYTHONSITE environment variable (separated by :)
|
||||||
|
and load the ones already present in sys.path via site.addsitedir()
|
||||||
|
to handle .pth files in them.
|
||||||
|
|
||||||
|
This is needed to properly import old-style namespace packages with nspkg.pth files.
|
||||||
|
See https://bugzilla.redhat.com/2018551 for a more detailed rationale.'''
|
||||||
|
for path in os.getenv('_PYTHONSITE', '').split(':'):
|
||||||
|
if path in sys.path:
|
||||||
|
site.addsitedir(path)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
|
||||||
|
cli_args = argparser().parse_args(argv)
|
||||||
|
|
||||||
|
if not cli_args.modules and not cli_args.filename:
|
||||||
|
raise ValueError('No modules to check were provided')
|
||||||
|
|
||||||
|
modules = read_modules_from_all_args(cli_args)
|
||||||
|
|
||||||
|
with remove_unwanteds_from_sys_path():
|
||||||
|
addsitedirs_from_environ()
|
||||||
|
import_modules(modules)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -61,7 +61,7 @@
|
|||||||
if rpm.expand("%{?py3_shebang_flags}") ~= "" then
|
if rpm.expand("%{?py3_shebang_flags}") ~= "" then
|
||||||
command = command .. "-%{py3_shebang_flags}"
|
command = command .. "-%{py3_shebang_flags}"
|
||||||
end
|
end
|
||||||
command = command .. " %{_rpmconfigdir}/redhat/import_all_modules.py "
|
command = command .. " %{_rpmconfigdir}/redhat/import_all_modules_py3_11.py "
|
||||||
-- handle multiline arguments correctly, see https://bugzilla.redhat.com/2018809
|
-- handle multiline arguments correctly, see https://bugzilla.redhat.com/2018809
|
||||||
local args=rpm.expand('%{?**}'):gsub("[%s\\\\]*%s+", " ")
|
local args=rpm.expand('%{?**}'):gsub("[%s\\\\]*%s+", " ")
|
||||||
print(command .. args)
|
print(command .. args)
|
||||||
|
@ -16,11 +16,11 @@ URL: https://www.python.org/
|
|||||||
|
|
||||||
# WARNING When rebasing to a new Python version,
|
# WARNING When rebasing to a new Python version,
|
||||||
# remember to update the python3-docs package as well
|
# remember to update the python3-docs package as well
|
||||||
%global general_version %{pybasever}.2
|
%global general_version %{pybasever}.5
|
||||||
#global prerel ...
|
#global prerel ...
|
||||||
%global upstream_version %{general_version}%{?prerel}
|
%global upstream_version %{general_version}%{?prerel}
|
||||||
Version: %{general_version}%{?prerel:~%{prerel}}
|
Version: %{general_version}%{?prerel:~%{prerel}}
|
||||||
Release: 2%{?dist}.2
|
Release: 1%{?dist}
|
||||||
License: Python
|
License: Python
|
||||||
|
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ License: Python
|
|||||||
# If the rpmwheels condition is disabled, we use the bundled wheel packages
|
# If the rpmwheels condition is disabled, we use the bundled wheel packages
|
||||||
# from Python with the versions below.
|
# from Python with the versions below.
|
||||||
# This needs to be manually updated when we update Python.
|
# This needs to be manually updated when we update Python.
|
||||||
%global pip_version 22.3.1
|
%global pip_version 23.2.1
|
||||||
%global setuptools_version 65.5.0
|
%global setuptools_version 65.5.0
|
||||||
|
|
||||||
# Expensive optimizations (mainly, profile-guided optimizations)
|
# Expensive optimizations (mainly, profile-guided optimizations)
|
||||||
@ -252,7 +252,10 @@ Source0: %{url}ftp/python/%{general_version}/Python-%{upstream_version}.tar.xz
|
|||||||
Source1: %{url}ftp/python/%{general_version}/Python-%{upstream_version}.tar.xz.asc
|
Source1: %{url}ftp/python/%{general_version}/Python-%{upstream_version}.tar.xz.asc
|
||||||
# The release manager for Python 3.11 is pablogsal
|
# The release manager for Python 3.11 is pablogsal
|
||||||
Source2: https://keybase.io/pablogsal/pgp_keys.asc
|
Source2: https://keybase.io/pablogsal/pgp_keys.asc
|
||||||
|
|
||||||
|
# Sources for the python3.11-rpm-macros
|
||||||
Source3: macros.python3.11
|
Source3: macros.python3.11
|
||||||
|
Source4: import_all_modules_py3_11.py
|
||||||
|
|
||||||
# A simple script to check timestamps of bytecode files
|
# A simple script to check timestamps of bytecode files
|
||||||
# Run in check section with Python that is currently being built
|
# Run in check section with Python that is currently being built
|
||||||
@ -358,27 +361,15 @@ Patch371: 00371-revert-bpo-1596321-fix-threading-_shutdown-for-the-main-thread-g
|
|||||||
# Upstream: https://bugs.python.org/issue46811
|
# Upstream: https://bugs.python.org/issue46811
|
||||||
Patch378: 00378-support-expat-2-4-5.patch
|
Patch378: 00378-support-expat-2-4-5.patch
|
||||||
|
|
||||||
# 00399 # 62614243969f1c717a02a1c65e55ef173ad9a6dd
|
# 00397 #
|
||||||
# CVE-2023-24329
|
# Filters for tarfile extraction (CVE-2007-4559, PEP-706)
|
||||||
#
|
# First patch fixes determination of symlink targets, which were treated
|
||||||
# * gh-102153: Start stripping C0 control and space chars in `urlsplit` (GH-102508)
|
# as relative to the root of the archive,
|
||||||
#
|
# rather than the directory containing the symlink.
|
||||||
# `urllib.parse.urlsplit` has already been respecting the WHATWG spec a bit GH-25595.
|
# Not yet upstream as of this writing.
|
||||||
#
|
# The second patch is Red Hat configuration, see KB for documentation:
|
||||||
# This adds more sanitizing to respect the "Remove any leading C0 control or space from input" [rule](https://url.spec.whatwg.org/GH-url-parsing:~:text=Remove%%20any%%20leading%%20and%%20trailing%%20C0%%20control%%20or%%20space%%20from%%20input.) in response to [CVE-2023-24329](https://nvd.nist.gov/vuln/detail/CVE-2023-24329).
|
# - https://access.redhat.com/articles/7004769
|
||||||
#
|
Patch397: 00397-tarfile-filter.patch
|
||||||
# ---------
|
|
||||||
Patch399: 00399-cve-2023-24329.patch
|
|
||||||
|
|
||||||
# 00404 #
|
|
||||||
# CVE-2023-40217
|
|
||||||
#
|
|
||||||
# Security fix for CVE-2023-40217: Bypass TLS handshake on closed sockets
|
|
||||||
# Resolved upstream: https://github.com/python/cpython/issues/108310
|
|
||||||
# Fixups added on top from:
|
|
||||||
# https://github.com/python/cpython/issues/108342
|
|
||||||
#
|
|
||||||
Patch404: 00404-cve-2023-40217.patch
|
|
||||||
|
|
||||||
# (New patches go here ^^^)
|
# (New patches go here ^^^)
|
||||||
#
|
#
|
||||||
@ -1120,6 +1111,10 @@ mkdir -p %{buildroot}%{rpmmacrodir}/
|
|||||||
install -m 644 %{SOURCE3} \
|
install -m 644 %{SOURCE3} \
|
||||||
%{buildroot}/%{rpmmacrodir}/
|
%{buildroot}/%{rpmmacrodir}/
|
||||||
|
|
||||||
|
# Add a script that is being used by python3.11-rpm-macros
|
||||||
|
mkdir -p %{buildroot}%{_rpmconfigdir}/redhat
|
||||||
|
install -m 644 %{SOURCE4} %{buildroot}%{_rpmconfigdir}/redhat/
|
||||||
|
|
||||||
# All ghost files controlled by alternatives need to exist for the files
|
# All ghost files controlled by alternatives need to exist for the files
|
||||||
# section check to succeed
|
# section check to succeed
|
||||||
# - Don't list /usr/bin/python as a ghost file so `yum install /usr/bin/python`
|
# - Don't list /usr/bin/python as a ghost file so `yum install /usr/bin/python`
|
||||||
@ -1194,10 +1189,14 @@ CheckPython() {
|
|||||||
# test_freeze_simple_script is skipped, because it fails when bundled wheels
|
# test_freeze_simple_script is skipped, because it fails when bundled wheels
|
||||||
# are removed in Fedora.
|
# are removed in Fedora.
|
||||||
# upstream report: https://bugs.python.org/issue45783
|
# upstream report: https://bugs.python.org/issue45783
|
||||||
|
# test_check_probes is failing since it was introduced in 3.11.5,
|
||||||
|
# the test is skipped until it is fixed in upstream.
|
||||||
|
# see: https://github.com/python/cpython/issues/104280#issuecomment-1669249980
|
||||||
|
|
||||||
LD_LIBRARY_PATH=$ConfDir $ConfDir/python -m test.regrtest \
|
LD_LIBRARY_PATH=$ConfDir $ConfDir/python -m test.regrtest \
|
||||||
-wW --slowest -j0 --timeout=1800 \
|
-wW --slowest -j0 --timeout=1800 \
|
||||||
-i test_freeze_simple_script \
|
-i test_freeze_simple_script \
|
||||||
|
-i test_check_probes \
|
||||||
%if %{with bootstrap}
|
%if %{with bootstrap}
|
||||||
-x test_distutils \
|
-x test_distutils \
|
||||||
%endif
|
%endif
|
||||||
@ -1318,6 +1317,7 @@ fi
|
|||||||
|
|
||||||
%files -n %{pkgname}-rpm-macros
|
%files -n %{pkgname}-rpm-macros
|
||||||
%{rpmmacrodir}/macros.python%{pybasever}
|
%{rpmmacrodir}/macros.python%{pybasever}
|
||||||
|
%{_rpmconfigdir}/redhat/import_all_modules_py3_11.py
|
||||||
|
|
||||||
%files -n %{pkgname}
|
%files -n %{pkgname}
|
||||||
%doc README.rst
|
%doc README.rst
|
||||||
@ -1821,11 +1821,25 @@ fi
|
|||||||
# ======================================================
|
# ======================================================
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
* Tue Sep 12 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.11.2-2.2
|
* Thu Sep 07 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.11.5-1
|
||||||
- Security fix for CVE-2023-40217
|
- Rebase to 3.11.5
|
||||||
Resolves: rhbz#2235789
|
- Security fixes for CVE-2023-40217 and CVE-2023-41105
|
||||||
|
Resolves: RHEL-3047, RHEL-3267
|
||||||
|
|
||||||
* Wed May 24 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.11.2-2.1
|
* Thu Aug 10 2023 Tomas Orsava <torsava@redhat.com> - 3.11.4-4
|
||||||
|
- Add the import_all_modules_py3_11.py file for the python3.11-rpm-macros subpackage
|
||||||
|
Resolves: rhbz#2207631
|
||||||
|
|
||||||
|
* Wed Aug 09 2023 Petr Viktorin <pviktori@redhat.com> - 3.11.4-3
|
||||||
|
- Fix symlink handling in the fix for CVE-2023-24329
|
||||||
|
Resolves: rhbz#263261
|
||||||
|
|
||||||
|
* Fri Jun 30 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.11.4-2
|
||||||
|
- Security fix for CVE-2007-4559
|
||||||
|
Resolves: rhbz#263261
|
||||||
|
|
||||||
|
* Mon Jun 26 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.11.4-1
|
||||||
|
- Update to 3.11.4
|
||||||
- Security fix for CVE-2023-24329
|
- Security fix for CVE-2023-24329
|
||||||
Resolves: rhbz#2173917
|
Resolves: rhbz#2173917
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user