:class:`SimpleHTTPRequestHandler` will follow symbolic links when handling
requests, this makes it possible for files outside of the specified directory
to be served.
+
+Earlier versions of Python did not scrub control characters from the
+log messages emitted to stderr from ``python -m http.server`` or the
+default :class:`BaseHTTPRequestHandler` ``.log_message``
+implementation. This could allow remote clients connecting to your
+server to send nefarious control codes to your terminal.
+
+.. versionadded:: 3.9.16
+ scrubbing control characters from log messages
use) to determine whether or not the mailcap line applies. :func:`findmatch`
will automatically check such conditions and skip the entry if the check fails.
+ .. versionchanged:: 3.11
+
+ To prevent security issues with shell metacharacters (symbols that have
+ special effects in a shell command line), ``findmatch`` will refuse
+ to inject ASCII characters other than alphanumerics and ``@+=:,./-_``
+ into the returned command line.
+
+ If a disallowed character appears in *filename*, ``findmatch`` will always
+ return ``(None, None)`` as if no entry was found.
+ If such a character appears elsewhere (a value in *plist* or in *MIMEtype*),
+ ``findmatch`` will ignore all mailcap entries which use that value.
+ A :mod:`warning <warnings>` will be raised in either case.
.. function:: getcaps()
/*--start constants--*/
#define PY_MAJOR_VERSION 3
#define PY_MINOR_VERSION 9
-#define PY_MICRO_VERSION 15
+#define PY_MICRO_VERSION 16
#define PY_RELEASE_LEVEL PY_RELEASE_LEVEL_FINAL
#define PY_RELEASE_SERIAL 0
/* Version as a string */
-#define PY_VERSION "3.9.15"
+#define PY_VERSION "3.9.16"
/*--end constants--*/
/* Version as a single 4-byte hex number, e.g. 0x010502B2 == 1.5.2b2.
# Check bidi
RandAL = [stringprep.in_table_d1(x) for x in label]
- for c in RandAL:
- if c:
- # There is a RandAL char in the string. Must perform further
- # tests:
- # 1) The characters in section 5.8 MUST be prohibited.
- # This is table C.8, which was already checked
- # 2) If a string contains any RandALCat character, the string
- # MUST NOT contain any LCat character.
- if any(stringprep.in_table_d2(x) for x in label):
- raise UnicodeError("Violation of BIDI requirement 2")
-
- # 3) If a string contains any RandALCat character, a
- # RandALCat character MUST be the first character of the
- # string, and a RandALCat character MUST be the last
- # character of the string.
- if not RandAL[0] or not RandAL[-1]:
- raise UnicodeError("Violation of BIDI requirement 3")
+ if any(RandAL):
+ # There is a RandAL char in the string. Must perform further
+ # tests:
+ # 1) The characters in section 5.8 MUST be prohibited.
+ # This is table C.8, which was already checked
+ # 2) If a string contains any RandALCat character, the string
+ # MUST NOT contain any LCat character.
+ if any(stringprep.in_table_d2(x) for x in label):
+ raise UnicodeError("Violation of BIDI requirement 2")
+ # 3) If a string contains any RandALCat character, a
+ # RandALCat character MUST be the first character of the
+ # string, and a RandALCat character MUST be the last
+ # character of the string.
+ if not RandAL[0] or not RandAL[-1]:
+ raise UnicodeError("Violation of BIDI requirement 3")
return label
import html
import http.client
import io
+import itertools
import mimetypes
import os
import posixpath
self.log_message(format, *args)
+ # https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
+ _control_char_table = str.maketrans(
+ {c: fr'\x{c:02x}' for c in itertools.chain(range(0x20), range(0x7f,0xa0))})
+ _control_char_table[ord('\\')] = r'\\'
+
def log_message(self, format, *args):
"""Log an arbitrary message.
The client ip and current date/time are prefixed to
every message.
+ Unicode control characters are replaced with escaped hex
+ before writing the output to stderr.
+
"""
+ message = format % args
sys.stderr.write("%s - - [%s] %s\n" %
(self.address_string(),
self.log_date_time_string(),
- format%args))
+ message.translate(self._control_char_table)))
def version_string(self):
"""Return the server software version string."""
import os
import warnings
+import re
__all__ = ["getcaps","findmatch"]
else:
return 1, 0
+_find_unsafe = re.compile(r'[^\xa1-\U0010FFFF\w@+=:,./-]').search
+
+class UnsafeMailcapInput(Warning):
+ """Warning raised when refusing unsafe input"""
+
# Part 1: top-level interface.
entry to use.
"""
+ if _find_unsafe(filename):
+ msg = "Refusing to use mailcap with filename %r. Use a safe temporary filename." % (filename,)
+ warnings.warn(msg, UnsafeMailcapInput)
+ return None, None
entries = lookup(caps, MIMEtype, key)
# XXX This code should somehow check for the needsterminal flag.
for e in entries:
if 'test' in e:
test = subst(e['test'], filename, plist)
+ if test is None:
+ continue
if test and os.system(test) != 0:
continue
command = subst(e[key], MIMEtype, filename, plist)
- return command, e
+ if command is not None:
+ return command, e
return None, None
def lookup(caps, MIMEtype, key=None):
elif c == 's':
res = res + filename
elif c == 't':
+ if _find_unsafe(MIMEtype):
+ msg = "Refusing to substitute MIME type %r into a shell command." % (MIMEtype,)
+ warnings.warn(msg, UnsafeMailcapInput)
+ return None
res = res + MIMEtype
elif c == '{':
start = i
i = i+1
name = field[start:i]
i = i+1
- res = res + findparam(name, plist)
+ param = findparam(name, plist)
+ if _find_unsafe(param):
+ msg = "Refusing to substitute parameter %r (%s) into a shell command" % (param, name)
+ warnings.warn(msg, UnsafeMailcapInput)
+ return None
+ res = res + param
# XXX To do:
# %n == number of parts if type is multipart/*
# %F == list of alternating type and filename for parts
if family == 'AF_INET':
return ('localhost', 0)
elif family == 'AF_UNIX':
- # Prefer abstract sockets if possible to avoid problems with the address
- # size. When coding portable applications, some implementations have
- # sun_path as short as 92 bytes in the sockaddr_un struct.
- if util.abstract_sockets_supported:
- return f"\0listener-{os.getpid()}-{next(_mmap_counter)}"
return tempfile.mktemp(prefix='listener-', dir=util.get_temp_dir())
elif family == 'AF_PIPE':
return tempfile.mktemp(prefix=r'\\.\pipe\pyc-%d-%d-' %
gc.get_referents(y)
+def test_not_in_gc():
+ import gc
+
+ hook = lambda *a: None
+ sys.addaudithook(hook)
+
+ for o in gc.get_objects():
+ if isinstance(o, list):
+ assert hook not in o
+
+
if __name__ == "__main__":
from test.support import suppress_msvcrt_asserts
["gc.get_objects", "gc.get_referrers", "gc.get_referents"]
)
+ def test_not_in_gc(self):
+ returncode, _, stderr = self.run_python("test_not_in_gc")
+ if returncode:
+ self.fail(stderr)
+
if __name__ == "__main__":
unittest.main()
self.assertEqual("pyth\xf6n.org".encode("idna"), b"xn--pythn-mua.org")
self.assertEqual("pyth\xf6n.org.".encode("idna"), b"xn--pythn-mua.org.")
+ def test_builtin_decode_length_limit(self):
+ with self.assertRaisesRegex(UnicodeError, "too long"):
+ (b"xn--016c"+b"a"*1100).decode("idna")
+ with self.assertRaisesRegex(UnicodeError, "too long"):
+ (b"xn--016c"+b"a"*70).decode("idna")
+
def test_stream(self):
r = codecs.getreader("idna")(io.BytesIO(b"abc"))
r.read(3)
def test_case_md5_uintmax(self, size):
self.check('md5', b'A'*size, '28138d306ff1b8281f1a9067e1a1a2b3')
+ @unittest.skipIf(sys.maxsize < _4G - 1, 'test cannot run on 32-bit systems')
+ @bigmemtest(size=_4G - 1, memuse=1, dry_run=False)
+ def test_sha3_update_overflow(self, size):
+ """Regression test for gh-98517 CVE-2022-37454."""
+ h = hashlib.sha3_224()
+ h.update(b'\x01')
+ h.update(b'\x01'*0xffff_ffff)
+ self.assertEqual(h.hexdigest(), '80762e8ce6700f114fec0f621fd97c4b9c00147fa052215294cceeed')
+
# use the three examples from Federal Information Processing Standards
# Publication 180-1, Secure Hash Standard, 1995 April 17
# http://www.itl.nist.gov/div897/pubs/fip180-1.htm
import datetime
import threading
from unittest import mock
-from io import BytesIO
+from io import BytesIO, StringIO
import unittest
from test import support
match = self.HTTPResponseMatch.search(response)
self.assertIsNotNone(match)
+ def test_unprintable_not_logged(self):
+ # We call the method from the class directly as our Socketless
+ # Handler subclass overrode it... nice for everything BUT this test.
+ self.handler.client_address = ('127.0.0.1', 1337)
+ log_message = BaseHTTPRequestHandler.log_message
+ with mock.patch.object(sys, 'stderr', StringIO()) as fake_stderr:
+ log_message(self.handler, '/foo')
+ log_message(self.handler, '/\033bar\000\033')
+ log_message(self.handler, '/spam %s.', 'a')
+ log_message(self.handler, '/spam %s.', '\033\x7f\x9f\xa0beans')
+ stderr = fake_stderr.getvalue()
+ self.assertNotIn('\033', stderr) # non-printable chars are caught.
+ self.assertNotIn('\000', stderr) # non-printable chars are caught.
+ lines = stderr.splitlines()
+ self.assertIn('/foo', lines[0])
+ self.assertIn(r'/\x1bbar\x00\x1b', lines[1])
+ self.assertIn('/spam a.', lines[2])
+ self.assertIn('/spam \\x1b\\x7f\\x9f\xa0beans.', lines[3])
+
def test_http_1_1(self):
result = self.send_typical_request(b'GET / HTTP/1.1\r\n\r\n')
self.verify_http_server_response(result[0])
self.assertEqual(len(huge_decimal), digits)
# Ensuring that we chose a slow enough conversion to measure.
# It takes 0.1 seconds on a Zen based cloud VM in an opt build.
- if seconds_to_convert < 0.005:
+ # Some OSes have a low res 1/64s timer, skip if hard to measure.
+ if seconds_to_convert < 1/64:
raise unittest.SkipTest('"slow" conversion took only '
f'{seconds_to_convert} seconds.')
str(huge_int)
seconds_to_fail_huge = get_time() - start
self.assertIn('conversion', str(err.exception))
- self.assertLess(seconds_to_fail_huge, seconds_to_convert/8)
+ self.assertLessEqual(seconds_to_fail_huge, seconds_to_convert/2)
# Now we test that a conversion that would take 30x as long also fails
# in a similarly fast fashion.
str(extra_huge_int)
seconds_to_fail_extra_huge = get_time() - start
self.assertIn('conversion', str(err.exception))
- self.assertLess(seconds_to_fail_extra_huge, seconds_to_convert/8)
+ self.assertLess(seconds_to_fail_extra_huge, seconds_to_convert/2)
def test_denial_of_service_prevented_str_to_int(self):
"""Regression test: ensure we fail before performing O(N**2) work."""
seconds_to_convert = get_time() - start
# Ensuring that we chose a slow enough conversion to measure.
# It takes 0.1 seconds on a Zen based cloud VM in an opt build.
- if seconds_to_convert < 0.005:
+ # Some OSes have a low res 1/64s timer, skip if hard to measure.
+ if seconds_to_convert < 1/64:
raise unittest.SkipTest('"slow" conversion took only '
f'{seconds_to_convert} seconds.')
int(huge)
seconds_to_fail_huge = get_time() - start
self.assertIn('conversion', str(err.exception))
- self.assertLess(seconds_to_fail_huge, seconds_to_convert/8)
+ self.assertLessEqual(seconds_to_fail_huge, seconds_to_convert/2)
# Now we test that a conversion that would take 30x as long also fails
# in a similarly fast fashion.
int(extra_huge)
seconds_to_fail_extra_huge = get_time() - start
self.assertIn('conversion', str(err.exception))
- self.assertLess(seconds_to_fail_extra_huge, seconds_to_convert/8)
+ self.assertLessEqual(seconds_to_fail_extra_huge, seconds_to_convert/2)
def test_power_of_two_bases_unlimited(self):
"""The limit does not apply to power of 2 bases."""
(["", "audio/*", "foo.txt"], ""),
(["echo foo", "audio/*", "foo.txt"], "echo foo"),
(["echo %s", "audio/*", "foo.txt"], "echo foo.txt"),
- (["echo %t", "audio/*", "foo.txt"], "echo audio/*"),
+ (["echo %t", "audio/*", "foo.txt"], None),
+ (["echo %t", "audio/wav", "foo.txt"], "echo audio/wav"),
(["echo \\%t", "audio/*", "foo.txt"], "echo %t"),
(["echo foo", "audio/*", "foo.txt", plist], "echo foo"),
(["echo %{total}", "audio/*", "foo.txt", plist], "echo 3")
('"An audio fragment"', audio_basic_entry)),
([c, "audio/*"],
{"filename": fname},
- ("/usr/local/bin/showaudio audio/*", audio_entry)),
+ (None, None)),
+ ([c, "audio/wav"],
+ {"filename": fname},
+ ("/usr/local/bin/showaudio audio/wav", audio_entry)),
([c, "message/external-body"],
{"plist": plist},
("showexternal /dev/null default john python.org /tmp foo bar", message_entry))
Python News
+++++++++++
+What's New in Python 3.9.16 final?
+==================================
+
+*Release date: 2022-12-06*
+
+Security
+--------
+
+- gh-issue-100001: ``python -m http.server`` no longer allows terminal
+ control characters sent within a garbage request to be printed to the
+ stderr server log.
+
+ This is done by changing the :mod:`http.server`
+ :class:`BaseHTTPRequestHandler` ``.log_message`` method to replace control
+ characters with a ``\xHH`` hex escape before printing.
+
+- gh-issue-87604: Avoid publishing list of active per-interpreter audit
+ hooks via the :mod:`gc` module
+
+- gh-issue-98433: The IDNA codec decoder used on DNS hostnames by
+ :mod:`socket` or :mod:`asyncio` related name resolution functions no
+ longer involves a quadratic algorithm. This prevents a potential CPU
+ denial of service if an out-of-spec excessive length hostname involving
+ bidirectional characters were decoded. Some protocols such as
+ :mod:`urllib` http ``3xx`` redirects potentially allow for an attacker to
+ supply such a name.
+
+- gh-issue-98739: Update bundled libexpat to 2.5.0
+
+- gh-issue-98517: Port XKCP's fix for the buffer overflows in SHA-3
+ (CVE-2022-37454).
+
+- gh-issue-97514: On Linux the :mod:`multiprocessing` module returns to
+ using filesystem backed unix domain sockets for communication with the
+ *forkserver* process instead of the Linux abstract socket namespace. Only
+ code that chooses to use the :ref:`"forkserver" start method
+ <multiprocessing-start-methods>` is affected.
+
+ Abstract sockets have no permissions and could allow any user on the
+ system in the same `network namespace
+ <https://man7.org/linux/man-pages/man7/network_namespaces.7.html>`_ (often
+ the whole system) to inject code into the multiprocessing *forkserver*
+ process. This was a potential privilege escalation. Filesystem based
+ socket permissions restrict this to the *forkserver* process user as was
+ the default in Python 3.8 and earlier.
+
+ This prevents Linux `CVE-2022-42919
+ <https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-42919>`_.
+
+- gh-issue-68966: The deprecated mailcap module now refuses to inject unsafe
+ text (filenames, MIME types, parameters) into shell commands. Instead of
+ using such text, it will warn and act as if a match was not found (or for
+ test commands, as if the test failed).
+
+
What's New in Python 3.9.15 final?
==================================
i = 0;
curData = data;
while(i < dataByteLen) {
- if ((instance->byteIOIndex == 0) && (dataByteLen >= (i + rateInBytes))) {
+ if ((instance->byteIOIndex == 0) && (dataByteLen-i >= rateInBytes)) {
#ifdef SnP_FastLoop_Absorb
/* processing full blocks first */
}
else {
/* normal lane: using the message queue */
-
- partialBlock = (unsigned int)(dataByteLen - i);
- if (partialBlock+instance->byteIOIndex > rateInBytes)
+ if (dataByteLen-i > rateInBytes-instance->byteIOIndex)
partialBlock = rateInBytes-instance->byteIOIndex;
+ else
+ partialBlock = (unsigned int)(dataByteLen - i);
#ifdef KeccakReference
displayBytes(1, "Block to be absorbed (part)", curData, partialBlock);
#endif
i = 0;
curData = data;
while(i < dataByteLen) {
- if ((instance->byteIOIndex == rateInBytes) && (dataByteLen >= (i + rateInBytes))) {
+ if ((instance->byteIOIndex == rateInBytes) && (dataByteLen-i >= rateInBytes)) {
for(j=dataByteLen-i; j>=rateInBytes; j-=rateInBytes) {
SnP_Permute(instance->state);
SnP_ExtractBytes(instance->state, curData, 0, rateInBytes);
SnP_Permute(instance->state);
instance->byteIOIndex = 0;
}
- partialBlock = (unsigned int)(dataByteLen - i);
- if (partialBlock+instance->byteIOIndex > rateInBytes)
+ if (dataByteLen-i > rateInBytes-instance->byteIOIndex)
partialBlock = rateInBytes-instance->byteIOIndex;
+ else
+ partialBlock = (unsigned int)(dataByteLen - i);
i += partialBlock;
SnP_ExtractBytes(instance->state, curData, instance->byteIOIndex, partialBlock);
See http://semver.org.
*/
#define XML_MAJOR_VERSION 2
-#define XML_MINOR_VERSION 4
-#define XML_MICRO_VERSION 9
+#define XML_MINOR_VERSION 5
+#define XML_MICRO_VERSION 0
#ifdef __cplusplus
}
-/* 90815a2b2c80c03b2b889fe1d427bb2b9e3282aa065e42784e001db4f23de324 (2.4.9+)
+/* 5ab094ffadd6edfc94c3eee53af44a86951f9f1f0933ada3114bbce2bfb02c99 (2.5.0+)
__ __ _
___\ \/ /_ __ __ _| |_
/ _ \\ /| '_ \ / _` | __|
Copyright (c) 2021 Dong-hee Na <donghee.na@python.org>
Copyright (c) 2022 Samanta Navarro <ferivoz@riseup.net>
Copyright (c) 2022 Jeffrey Walton <noloader@gmail.com>
+ Copyright (c) 2022 Jann Horn <jannh@google.com>
Licensed under the MIT license:
Permission is hereby granted, free of charge, to any person obtaining
parserInit(parser, encodingName);
if (encodingName && ! parser->m_protocolEncodingName) {
+ if (dtd) {
+ // We need to stop the upcoming call to XML_ParserFree from happily
+ // destroying parser->m_dtd because the DTD is shared with the parent
+ // parser and the only guard that keeps XML_ParserFree from destroying
+ // parser->m_dtd is parser->m_isParamEntity but it will be set to
+ // XML_TRUE only later in XML_ExternalEntityParserCreate (or not at all).
+ parser->m_dtd = NULL;
+ }
XML_ParserFree(parser);
return NULL;
}
int len;
const char *rawName;
TAG *tag = parser->m_tagStack;
- parser->m_tagStack = tag->parent;
- tag->parent = parser->m_freeTagList;
- parser->m_freeTagList = tag;
rawName = s + enc->minBytesPerChar * 2;
len = XmlNameLength(enc, rawName);
if (len != tag->rawNameLength
*eventPP = rawName;
return XML_ERROR_TAG_MISMATCH;
}
+ parser->m_tagStack = tag->parent;
+ tag->parent = parser->m_freeTagList;
+ parser->m_freeTagList = tag;
--parser->m_tagLevel;
if (parser->m_endElementHandler) {
const XML_Char *localPart;
parser->m_handlerArg, parser->m_declElementType->name,
parser->m_declAttributeId->name, parser->m_declAttributeType, 0,
role == XML_ROLE_REQUIRED_ATTRIBUTE_VALUE);
- poolClear(&parser->m_tempPool);
handleDefault = XML_FALSE;
}
}
+ poolClear(&parser->m_tempPool);
break;
case XML_ROLE_DEFAULT_ATTRIBUTE_VALUE:
case XML_ROLE_FIXED_ATTRIBUTE_VALUE:
*
* If 'standalone' is false, the DTD must have no
* parameter entities or we wouldn't have passed the outer
- * 'if' statement. That measn the only entity in the hash
+ * 'if' statement. That means the only entity in the hash
* table is the external subset name "#" which cannot be
* given as a parameter entity name in XML syntax, so the
* lookup must have returned NULL and we don't even reach
if (result != XML_ERROR_NONE)
return result;
- else if (textEnd != next
- && parser->m_parsingStatus.parsing == XML_SUSPENDED) {
+
+ if (textEnd != next && parser->m_parsingStatus.parsing == XML_SUSPENDED) {
entity->processed = (int)(next - (const char *)entity->textPtr);
return result;
- } else {
+ }
+
#ifdef XML_DTD
- entityTrackingOnClose(parser, entity, __LINE__);
+ entityTrackingOnClose(parser, entity, __LINE__);
#endif
- entity->open = XML_FALSE;
- parser->m_openInternalEntities = openEntity->next;
- /* put openEntity back in list of free instances */
- openEntity->next = parser->m_freeInternalEntities;
- parser->m_freeInternalEntities = openEntity;
+ entity->open = XML_FALSE;
+ parser->m_openInternalEntities = openEntity->next;
+ /* put openEntity back in list of free instances */
+ openEntity->next = parser->m_freeInternalEntities;
+ parser->m_freeInternalEntities = openEntity;
+
+ // If there are more open entities we want to stop right here and have the
+ // upcoming call to XML_ResumeParser continue with entity content, or it would
+ // be ignored altogether.
+ if (parser->m_openInternalEntities != NULL
+ && parser->m_parsingStatus.parsing == XML_SUSPENDED) {
+ return XML_ERROR_NONE;
}
#ifdef XML_DTD
BT_LF, /* line feed = "\n" */
BT_GT, /* greater than = ">" */
BT_QUOT, /* quotation character = "\"" */
- BT_APOS, /* aposthrophe = "'" */
+ BT_APOS, /* apostrophe = "'" */
BT_EQUALS, /* equal sign = "=" */
BT_QUEST, /* question mark = "?" */
BT_EXCL, /* exclamation mark = "!" */
if (is->audit_hooks == NULL) {
return NULL;
}
+ /* Avoid having our list of hooks show up in the GC module */
+ PyObject_GC_UnTrack(is->audit_hooks);
}
if (PyList_Append(is->audit_hooks, hook) < 0) {
-This is Python version 3.9.15
+This is Python version 3.9.16
=============================
.. image:: https://travis-ci.org/python/cpython.svg?branch=3.9