Skip to content

Commit f7eae0a

Browse files
[security] bpo-13617: Reject embedded null characters in wchar* strings. (#2302)
Based on patch by Victor Stinner. Add private C API function _PyUnicode_AsUnicode() which is similar to PyUnicode_AsUnicode(), but checks for null characters.
1 parent 592eda1 commit f7eae0a

22 files changed

+115
-23
lines changed

Include/unicodeobject.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -756,23 +756,27 @@ PyAPI_FUNC(Py_UCS4*) PyUnicode_AsUCS4(
756756
PyAPI_FUNC(Py_UCS4*) PyUnicode_AsUCS4Copy(PyObject *unicode);
757757
#endif
758758

759+
#ifndef Py_LIMITED_API
759760
/* Return a read-only pointer to the Unicode object's internal
760761
Py_UNICODE buffer.
761762
If the wchar_t/Py_UNICODE representation is not yet available, this
762763
function will calculate it. */
763764

764-
#ifndef Py_LIMITED_API
765765
PyAPI_FUNC(Py_UNICODE *) PyUnicode_AsUnicode(
766766
PyObject *unicode /* Unicode object */
767767
) /* Py_DEPRECATED(3.3) */;
768-
#endif
768+
769+
/* Similar to PyUnicode_AsUnicode(), but raises a ValueError if the string
770+
contains null characters. */
771+
PyAPI_FUNC(const Py_UNICODE *) _PyUnicode_AsUnicode(
772+
PyObject *unicode /* Unicode object */
773+
);
769774

770775
/* Return a read-only pointer to the Unicode object's internal
771776
Py_UNICODE buffer and save the length at size.
772777
If the wchar_t/Py_UNICODE representation is not yet available, this
773778
function will calculate it. */
774779

775-
#ifndef Py_LIMITED_API
776780
PyAPI_FUNC(Py_UNICODE *) PyUnicode_AsUnicodeAndSize(
777781
PyObject *unicode, /* Unicode object */
778782
Py_ssize_t *size /* location where to save the length */

Lib/ctypes/test/test_loading.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ def test_load_library(self):
6262
windll["kernel32"].GetModuleHandleW
6363
windll.LoadLibrary("kernel32").GetModuleHandleW
6464
WinDLL("kernel32").GetModuleHandleW
65+
# embedded null character
66+
self.assertRaises(ValueError, windll.LoadLibrary, "kernel32\0")
6567

6668
@unittest.skipUnless(os.name == "nt",
6769
'test specific to Windows')

Lib/test/test_builtin.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ def test_import(self):
151151
self.assertRaises(TypeError, __import__, 1, 2, 3, 4)
152152
self.assertRaises(ValueError, __import__, '')
153153
self.assertRaises(TypeError, __import__, 'sys', name='sys')
154+
# embedded null character
155+
self.assertRaises(ModuleNotFoundError, __import__, 'string\x00')
154156

155157
def test_abs(self):
156158
# int
@@ -1010,6 +1012,10 @@ def test_open(self):
10101012
self.assertEqual(fp.read(300), 'XXX'*100)
10111013
self.assertEqual(fp.read(1000), 'YYY'*100)
10121014

1015+
# embedded null bytes and characters
1016+
self.assertRaises(ValueError, open, 'a\x00b')
1017+
self.assertRaises(ValueError, open, b'a\x00b')
1018+
10131019
def test_open_default_encoding(self):
10141020
old_environ = dict(os.environ)
10151021
try:

Lib/test/test_curses.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def test_window_funcs(self):
8181
win2 = curses.newwin(15,15, 5,5)
8282

8383
for meth in [stdscr.addch, stdscr.addstr]:
84-
for args in [('a'), ('a', curses.A_BOLD),
84+
for args in [('a',), ('a', curses.A_BOLD),
8585
(4,4, 'a'), (5,5, 'a', curses.A_BOLD)]:
8686
with self.subTest(meth=meth.__qualname__, args=args):
8787
meth(*args)
@@ -194,6 +194,15 @@ def test_window_funcs(self):
194194
self.assertRaises(ValueError, stdscr.instr, -2)
195195
self.assertRaises(ValueError, stdscr.instr, 2, 3, -2)
196196

197+
def test_embedded_null_chars(self):
198+
# reject embedded null bytes and characters
199+
stdscr = self.stdscr
200+
for arg in ['a', b'a']:
201+
with self.subTest(arg=arg):
202+
self.assertRaises(ValueError, stdscr.addstr, 'a\0')
203+
self.assertRaises(ValueError, stdscr.addnstr, 'a\0', 1)
204+
self.assertRaises(ValueError, stdscr.insstr, 'a\0')
205+
self.assertRaises(ValueError, stdscr.insnstr, 'a\0', 1)
197206

198207
def test_module_funcs(self):
199208
"Test module-level functions"

Lib/test/test_grp.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ def test_errors(self):
5050
self.assertRaises(TypeError, grp.getgrgid)
5151
self.assertRaises(TypeError, grp.getgrnam)
5252
self.assertRaises(TypeError, grp.getgrall, 42)
53+
# embedded null character
54+
self.assertRaises(ValueError, grp.getgrnam, 'a\x00b')
5355

5456
# try to get some errors
5557
bynames = {}

Lib/test/test_imp.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,10 @@ def test_multiple_calls_to_get_data(self):
314314
loader.get_data(imp.__file__) # File should be closed
315315
loader.get_data(imp.__file__) # Will need to create a newly opened file
316316

317+
def test_load_source(self):
318+
with self.assertRaisesRegex(ValueError, 'embedded null'):
319+
imp.load_source(__name__, __file__ + "\0")
320+
317321

318322
class ReloadTests(unittest.TestCase):
319323

Lib/test/test_locale.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,9 +346,14 @@ def test_strcoll(self):
346346
self.assertLess(locale.strcoll('a', 'b'), 0)
347347
self.assertEqual(locale.strcoll('a', 'a'), 0)
348348
self.assertGreater(locale.strcoll('b', 'a'), 0)
349+
# embedded null character
350+
self.assertRaises(ValueError, locale.strcoll, 'a\0', 'a')
351+
self.assertRaises(ValueError, locale.strcoll, 'a', 'a\0')
349352

350353
def test_strxfrm(self):
351354
self.assertLess(locale.strxfrm('a'), locale.strxfrm('b'))
355+
# embedded null character
356+
self.assertRaises(ValueError, locale.strxfrm, 'a\0')
352357

353358

354359
class TestEnUSCollation(BaseLocalizedTest, TestCollation):

Lib/test/test_time.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ def test_strftime(self):
126126
except ValueError:
127127
self.fail('conversion specifier: %r failed.' % format)
128128

129+
self.assertRaises(TypeError, time.strftime, b'%S', tt)
130+
# embedded null character
131+
self.assertRaises(ValueError, time.strftime, '%S\0', tt)
132+
129133
def _bounds_checking(self, func):
130134
# Make sure that strftime() checks the bounds of the various parts
131135
# of the time tuple (0 is valid for *all* values).

Lib/test/test_winsound.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ def test_errors(self):
9696
self.assertRaises(TypeError, winsound.PlaySound, "bad",
9797
winsound.SND_MEMORY)
9898
self.assertRaises(TypeError, winsound.PlaySound, 1, 0)
99+
# embedded null character
100+
self.assertRaises(ValueError, winsound.PlaySound, 'bad\0', 0)
99101

100102
def test_keyword_args(self):
101103
safe_PlaySound(flags=winsound.SND_ALIAS, sound="SystemExit")

Modules/_ctypes/callproc.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,10 +1253,11 @@ static PyObject *load_library(PyObject *self, PyObject *args)
12531253
PyObject *nameobj;
12541254
PyObject *ignored;
12551255
HMODULE hMod;
1256-
if (!PyArg_ParseTuple(args, "O|O:LoadLibrary", &nameobj, &ignored))
1256+
1257+
if (!PyArg_ParseTuple(args, "U|O:LoadLibrary", &nameobj, &ignored))
12571258
return NULL;
12581259

1259-
name = PyUnicode_AsUnicode(nameobj);
1260+
name = _PyUnicode_AsUnicode(nameobj);
12601261
if (!name)
12611262
return NULL;
12621263

0 commit comments

Comments
 (0)