@@ -329,6 +329,55 @@ def test_willclose(self):
329329finally:
330330self.unfakehttp()
331331332+def test_url_with_control_char_rejected(self):
333+for char_no in list(range(0, 0x21)) + [0x7f]:
334+char = chr(char_no)
335+schemeless_url = f"//localhost:7777/test{char}/"
336+self.fakehttp(b"HTTP/1.1 200 OK\r\n\r\nHello.")
337+try:
338+# We explicitly test urllib.request.urlopen() instead of the top
339+# level 'def urlopen()' function defined in this... (quite ugly)
340+# test suite. They use different url opening codepaths. Plain
341+# urlopen uses FancyURLOpener which goes via a codepath that
342+# calls urllib.parse.quote() on the URL which makes all of the
343+# above attempts at injection within the url _path_ safe.
344+escaped_char_repr = repr(char).replace('\\', r'\\')
345+with self.assertRaisesRegex(
346+ValueError, f"contain control.*{escaped_char_repr}"):
347+urllib.request.urlopen(f"http:{schemeless_url}")
348+with self.assertRaisesRegex(
349+ValueError, f"contain control.*{escaped_char_repr}"):
350+urllib.request.urlopen(f"https:{schemeless_url}")
351+# This code path quotes the URL so there is no injection.
352+resp = urlopen(f"http:{schemeless_url}")
353+self.assertNotIn(char, resp.geturl())
354+finally:
355+self.unfakehttp()
356+357+def test_url_with_newline_header_injection_rejected(self):
358+self.fakehttp(b"HTTP/1.1 200 OK\r\n\r\nHello.")
359+host = "localhost:7777?a=1 HTTP/1.1\r\nX-injected: header\r\nTEST: 123"
360+schemeless_url = "//" + host + ":8080/test/?test=a"
361+try:
362+# We explicitly test urllib.request.urlopen() instead of the top
363+# level 'def urlopen()' function defined in this... (quite ugly)
364+# test suite. They use different url opening codepaths. Plain
365+# urlopen uses FancyURLOpener which goes via a codepath that
366+# calls urllib.parse.quote() on the URL which makes all of the
367+# above attempts at injection within the url _path_ safe.
368+with self.assertRaisesRegex(
369+ValueError, r"contain control.*\\r.*(found at least . .)"):
370+urllib.request.urlopen(f"http:{schemeless_url}")
371+with self.assertRaisesRegex(ValueError, r"contain control.*\\n"):
372+urllib.request.urlopen(f"https:{schemeless_url}")
373+# This code path quotes the URL so there is no injection.
374+resp = urlopen(f"http:{schemeless_url}")
375+self.assertNotIn(' ', resp.geturl())
376+self.assertNotIn('\r', resp.geturl())
377+self.assertNotIn('\n', resp.geturl())
378+finally:
379+self.unfakehttp()
380+332381def test_read_0_9(self):
333382# "0.9" response accepted (but not "simple responses" without
334383# a status line)