Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Doc/library/http.cookies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,9 @@ The following example demonstrates how to use the :mod:`http.cookies` module.
Set-Cookie: chips=ahoy
Set-Cookie: vienna=finger
>>> C = cookies.SimpleCookie()
>>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
>>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";')
>>> print(C)
Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;"
>>> C = cookies.SimpleCookie()
>>> C["oreo"] = "doublestuff"
>>> C["oreo"]["path"] = "/"
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ typedef struct _PyOptimizationConfig {

// Optimization flags
bool specialization_enabled;
bool uops_optimize_enabled;
} _PyOptimizationConfig;

struct
Expand Down
25 changes: 22 additions & 3 deletions Lib/http/cookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@
such trickeries do not confuse it.

>>> C = cookies.SimpleCookie()
>>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
>>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";')
>>> print(C)
Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;"

Each element of the Cookie also supports all of the RFC 2109
Cookie attributes. Here's an example which sets the Path
Expand Down Expand Up @@ -170,6 +170,15 @@ class CookieError(Exception):
})

_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
_control_character_re = re.compile(r'[\x00-\x1F\x7F]')


def _has_control_character(*val):
"""Detects control characters within a value.
Supports any type, as header values can be any type.
"""
return any(_control_character_re.search(str(v)) for v in val)


def _quote(str):
r"""Quote a string for use in a cookie header.
Expand Down Expand Up @@ -294,12 +303,16 @@ def __setitem__(self, K, V):
K = K.lower()
if not K in self._reserved:
raise CookieError("Invalid attribute %r" % (K,))
if _has_control_character(K, V):
raise CookieError(f"Control characters are not allowed in cookies {K!r} {V!r}")
dict.__setitem__(self, K, V)

def setdefault(self, key, val=None):
key = key.lower()
if key not in self._reserved:
raise CookieError("Invalid attribute %r" % (key,))
if _has_control_character(key, val):
raise CookieError("Control characters are not allowed in cookies %r %r" % (key, val,))
return dict.setdefault(self, key, val)

def __eq__(self, morsel):
Expand Down Expand Up @@ -335,6 +348,9 @@ def set(self, key, val, coded_val):
raise CookieError('Attempt to set a reserved key %r' % (key,))
if not _is_legal_key(key):
raise CookieError('Illegal key %r' % (key,))
if _has_control_character(key, val, coded_val):
raise CookieError(
"Control characters are not allowed in cookies %r %r %r" % (key, val, coded_val,))

# It's a good key, so save it.
self._key = key
Expand Down Expand Up @@ -488,7 +504,10 @@ def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"):
result = []
items = sorted(self.items())
for key, value in items:
result.append(value.output(attrs, header))
value_output = value.output(attrs, header)
if _has_control_character(value_output):
raise CookieError("Control characters are not allowed in cookies")
result.append(value_output)
return sep.join(result)

__str__ = output
Expand Down
4 changes: 3 additions & 1 deletion Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
# We compile these in _mode_xxx.
_Literal = br'.*{(?P<size>\d+)}$'
_Untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'

_control_chars = re.compile(b'[\x00-\x1F\x7F]')


class IMAP4:
Expand Down Expand Up @@ -1105,6 +1105,8 @@ def _command(self, name, *args):
if arg is None: continue
if isinstance(arg, str):
arg = bytes(arg, self._encoding)
if _control_chars.search(arg):
raise ValueError("Control characters not allowed in commands")
data = data + b' ' + arg

literal = self.literal
Expand Down
2 changes: 2 additions & 0 deletions Lib/poplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ def _putline(self, line):
def _putcmd(self, line):
if self._debugging: print('*cmd*', repr(line))
line = bytes(line, self.encoding)
if re.search(b'[\x00-\x1F\x7F]', line):
raise ValueError('Control characters not allowed in commands')
self._putline(line)


Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_coroutines.py
Original file line number Diff line number Diff line change
Expand Up @@ -2265,6 +2265,20 @@ def c():
# before fixing, visible stack from throw would be shorter than from send.
self.assertEqual(len_send, len_throw)

def test_call_generator_in_frame_clear(self):
# gh-143939: Running a generator while clearing the coroutine's frame
# should not be misinterpreted as a yield.
class CallGeneratorOnDealloc:
def __del__(self):
next(x for x in [1])

async def coro():
obj = CallGeneratorOnDealloc()
return 42

yielded, result = run_async(coro())
self.assertEqual(yielded, [])
self.assertEqual(result, 42)

@unittest.skipIf(
support.is_emscripten or support.is_wasi,
Expand Down
52 changes: 48 additions & 4 deletions Lib/test/test_http_cookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ def test_basic(self):
'repr': "<SimpleCookie: chips='ahoy' vienna='finger'>",
'output': 'Set-Cookie: chips=ahoy\nSet-Cookie: vienna=finger'},

{'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=\012;'},
'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=\\n;'>''',
'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"'},
{'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=;"',
'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=;'},
'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=;'>''',
'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=;"'},

# Check illegal cookies that have an '=' char in an unquoted value
{'data': 'keebler=E=mc2',
Expand Down Expand Up @@ -594,6 +594,50 @@ def test_repr(self):
r'Set-Cookie: key=coded_val; '
r'expires=\w+, \d+ \w+ \d+ \d+:\d+:\d+ \w+')

def test_control_characters(self):
for c0 in support.control_characters_c0():
morsel = cookies.Morsel()

# .__setitem__()
with self.assertRaises(cookies.CookieError):
morsel[c0] = "val"
with self.assertRaises(cookies.CookieError):
morsel["path"] = c0

# .setdefault()
with self.assertRaises(cookies.CookieError):
morsel.setdefault("path", c0)
with self.assertRaises(cookies.CookieError):
morsel.setdefault(c0, "val")

# .set()
with self.assertRaises(cookies.CookieError):
morsel.set(c0, "val", "coded-value")
with self.assertRaises(cookies.CookieError):
morsel.set("path", c0, "coded-value")
with self.assertRaises(cookies.CookieError):
morsel.set("path", "val", c0)

def test_control_characters_output(self):
# Tests that even if the internals of Morsel are modified
# that a call to .output() has control character safeguards.
for c0 in support.control_characters_c0():
morsel = cookies.Morsel()
morsel.set("key", "value", "coded-value")
morsel._key = c0 # Override private variable.
cookie = cookies.SimpleCookie()
cookie["cookie"] = morsel
with self.assertRaises(cookies.CookieError):
cookie.output()

morsel = cookies.Morsel()
morsel.set("key", "value", "coded-value")
morsel._coded_value = c0 # Override private variable.
cookie = cookies.SimpleCookie()
cookie["cookie"] = morsel
with self.assertRaises(cookies.CookieError):
cookie.output()


def load_tests(loader, tests, pattern):
tests.addTest(doctest.DocTestSuite(cookies))
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,12 @@ def test_unselect(self):
self.assertEqual(data[0], b'Returned to authenticated state. (Success)')
self.assertEqual(client.state, 'AUTH')

def test_control_characters(self):
client, _ = self._setup(SimpleIMAPHandler)
for c0 in support.control_characters_c0():
with self.assertRaises(ValueError):
client.login(f'user{c0}', 'pass')

# property tests

def test_file_property_should_not_be_accessed(self):
Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_poplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from test.support import threading_helper
from test.support import asynchat
from test.support import asyncore
from test.support import control_characters_c0


test_support.requires_working_socket(module=True)
Expand Down Expand Up @@ -395,6 +396,13 @@ def test_quit(self):
self.assertIsNone(self.client.sock)
self.assertIsNone(self.client.file)

def test_control_characters(self):
for c0 in control_characters_c0():
with self.assertRaises(ValueError):
self.client.user(f'user{c0}')
with self.assertRaises(ValueError):
self.client.pass_(f'{c0}pass')

@requires_ssl
def test_stls_capa(self):
capa = self.client.capa()
Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_urllib.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from test import support
from test.support import os_helper
from test.support import socket_helper
from test.support import control_characters_c0
import os
import socket
try:
Expand Down Expand Up @@ -590,6 +591,13 @@ def test_invalid_base64_data(self):
# missing padding character
self.assertRaises(ValueError,urllib.request.urlopen,'data:;base64,Cg=')

def test_invalid_mediatype(self):
for c0 in control_characters_c0():
self.assertRaises(ValueError,urllib.request.urlopen,
f'data:text/html;{c0},data')
for c0 in control_characters_c0():
self.assertRaises(ValueError,urllib.request.urlopen,
f'data:text/html{c0};base64,ZGF0YQ==')

class urlretrieve_FileTests(unittest.TestCase):
"""Test urllib.urlretrieve() on local files"""
Expand Down
5 changes: 5 additions & 0 deletions Lib/urllib/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -1636,6 +1636,11 @@ def data_open(self, req):
scheme, data = url.split(":",1)
mediatype, data = data.split(",",1)

# Disallow control characters within mediatype.
if re.search(r"[\x00-\x1F\x7F]", mediatype):
raise ValueError(
"Control characters not allowed in data: mediatype")

# even base64 encoded data URLs might be quoted so unquote in any case:
data = unquote_to_bytes(data)
if mediatype.endswith(";base64"):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix erroneous "cannot reuse already awaited coroutine" error that could
occur when a generator was run during the process of clearing a coroutine's
frame.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reject control characters in :class:`http.cookies.Morsel` fields and values.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reject control characters in IMAP commands.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reject control characters in POP3 commands.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reject control characters in ``data:`` URL media types.
3 changes: 3 additions & 0 deletions Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ gen_send_ex2(PyGenObject *gen, PyObject *arg, PyObject **presult, int exc)

if (return_kind == GENERATOR_YIELD) {
assert(result != NULL && !_PyErr_Occurred(tstate));
#ifndef Py_GIL_DISABLED
assert(FRAME_STATE_SUSPENDED(gen->gi_frame_state));
#endif
*presult = result;
return PYGEN_NEXT;
}
Expand Down
4 changes: 3 additions & 1 deletion Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -1914,14 +1914,16 @@ clear_gen_frame(PyThreadState *tstate, _PyInterpreterFrame * frame)
assert(frame->owner == FRAME_OWNED_BY_GENERATOR);
PyGenObject *gen = _PyGen_GetGeneratorFromFrame(frame);
FT_ATOMIC_STORE_INT8_RELEASE(gen->gi_frame_state, FRAME_CLEARED);
((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_RETURN;
assert(tstate->exc_info == &gen->gi_exc_state);
tstate->exc_info = gen->gi_exc_state.previous_item;
gen->gi_exc_state.previous_item = NULL;
assert(frame->frame_obj == NULL || frame->frame_obj->f_frame == frame);
frame->previous = NULL;
_PyFrame_ClearExceptCode(frame);
_PyErr_ClearExcState(&gen->gi_exc_state);
// gh-143939: There must not be any escaping calls between setting
// the generator return kind and returning from _PyEval_EvalFrame.
((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_RETURN;
}

void
Expand Down
Loading
Loading