Skip to content

Commit c88f078

Browse files
gh-64470: Validate the port in urlsplit() and urlparse()
urlsplit() and urlparse() now raise ValueError for an URL with an invalid port, instead of failing only when the port attribute of the result is read. This also rejects URLs with an unbracketed IPv6 address, like "http://::1/". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent caac927 commit c88f078

5 files changed

Lines changed: 50 additions & 14 deletions

File tree

Doc/library/urllib.parse.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,9 @@ or on combining URL components into a URL string.
162162

163163
.. [1] Depending on the value of the *missing_as_none* argument.
164164
165-
Reading the :attr:`port` attribute will raise a :exc:`ValueError` if
166-
an invalid port is specified in the URL. See section
165+
An invalid port specified in the URL will raise a :exc:`ValueError`.
166+
Reading the :attr:`port` attribute of a result object created directly
167+
will raise a :exc:`ValueError` too. See section
167168
:ref:`urlparse-result-object` for more information on the result object.
168169

169170
Unmatched square brackets in the :attr:`netloc` attribute will raise a
@@ -225,6 +226,11 @@ or on combining URL components into a URL string.
225226
.. versionchanged:: 3.15
226227
Added the *missing_as_none* parameter.
227228

229+
.. versionchanged:: next
230+
An invalid port now raises :exc:`ValueError` when the URL is parsed,
231+
not only when the :attr:`~urllib.parse.SplitResult.port` attribute is
232+
read.
233+
228234
.. _WHATWG spec: https://url.spec.whatwg.org/#concept-basic-url-parser
229235

230236

Lib/test/test_urllib.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -400,12 +400,16 @@ def test_url_host_with_newline_header_injection_rejected(self):
400400
host = "localhost\r\nX-injected: header\r\n"
401401
schemeless_url = "//" + host + ":8080/test/?test=a"
402402
try:
403-
InvalidURL = http.client.InvalidURL
404-
with self.assertRaisesRegex(
405-
InvalidURL, r"contain control.*\\r"):
403+
# The URL is rejected when it is parsed, because the injected
404+
# header is not a valid port.
405+
with self.assertRaises(ValueError):
406406
urllib.request.urlopen(f"http:{schemeless_url}")
407-
with self.assertRaisesRegex(InvalidURL, r"contain control.*\\n"):
407+
with self.assertRaises(ValueError):
408408
urllib.request.urlopen(f"https:{schemeless_url}")
409+
# Such host is rejected by http.client as well.
410+
with self.assertRaisesRegex(http.client.InvalidURL,
411+
r"contain control.*\\r"):
412+
http.client.HTTPConnection(host, 8080)
409413
finally:
410414
self.unfakehttp()
411415

Lib/test/test_urlparse.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -917,9 +917,8 @@ def test_urlsplit_attributes(self):
917917

918918
# Verify an illegal port raises ValueError
919919
url = b"HTTP://WWW.PYTHON.ORG:65536/doc/#frag"
920-
p = urllib.parse.urlsplit(url)
921920
with self.assertRaisesRegex(ValueError, "out of range"):
922-
p.port
921+
urllib.parse.urlsplit(url)
923922

924923
def test_urlsplit_remove_unsafe_bytes(self):
925924
# Remove ASCII tabs and newlines from input
@@ -1029,11 +1028,32 @@ def test_attributes_bad_port(self, bytes, parse, port):
10291028
self.skipTest('non-ASCII bytes')
10301029
netloc = str_encode(netloc)
10311030
url = str_encode(url)
1032-
p = parse(url)
1033-
self.assertEqual(p.netloc, netloc)
1031+
with self.assertRaises(ValueError):
1032+
parse(url)
1033+
# The port is still checked when it is read from a result
1034+
# constructed directly.
1035+
if bytes:
1036+
p = urllib.parse.SplitResultBytes(b'http', netloc, b'/', b'', b'')
1037+
else:
1038+
p = urllib.parse.SplitResult('http', netloc, '/', '', '')
10341039
with self.assertRaises(ValueError):
10351040
p.port
10361041

1042+
@support.subTests('parse', (urllib.parse.urlsplit, urllib.parse.urlparse))
1043+
@support.subTests('netloc', ("::1", "a:b:c", "user@::1", "[::1]:80:80"))
1044+
def test_attributes_bad_netloc_port(self, parse, netloc):
1045+
"""Check handling of a colon which does not delimit a valid port."""
1046+
with self.assertRaises(ValueError):
1047+
parse("http://" + netloc + "/")
1048+
1049+
@support.subTests('parse', (urllib.parse.urlsplit, urllib.parse.urlparse))
1050+
@support.subTests('netloc', ("www.example.net", "www.example.net:",
1051+
"user:password@www.example.net",
1052+
"[::1]", "[::1]:80", "[::1]:"))
1053+
def test_attributes_good_port(self, parse, netloc):
1054+
"""Check that valid netlocs are not rejected."""
1055+
self.assertEqual(parse("http://" + netloc + "/").netloc, netloc)
1056+
10371057
@support.subTests('bytes', (False, True))
10381058
@support.subTests('parse', (urllib.parse.urlsplit, urllib.parse.urlparse))
10391059
@support.subTests('scheme', (".", "+", "-", "0", "http&", "६http"))
@@ -1670,13 +1690,11 @@ def test_splitting_bracketed_hosts(self):
16701690

16711691
def test_port_casting_failure_message(self):
16721692
message = "Port could not be cast to integer value as 'oracle'"
1673-
p1 = urllib.parse.urlparse('http://Server=sde; Service=sde:oracle')
16741693
with self.assertRaisesRegex(ValueError, message):
1675-
p1.port
1694+
urllib.parse.urlparse('http://Server=sde; Service=sde:oracle')
16761695

1677-
p2 = urllib.parse.urlsplit('http://Server=sde; Service=sde:oracle')
16781696
with self.assertRaisesRegex(ValueError, message):
1679-
p2.port
1697+
urllib.parse.urlsplit('http://Server=sde; Service=sde:oracle')
16801698

16811699
def test_telurl_params(self):
16821700
p1 = urllib.parse.urlparse('tel:123-4;phone-context=+1-650-516')

Lib/urllib/parse.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,8 @@ def urlparse(url, scheme=None, allow_fragments=True, *, missing_as_none=_MISSING
477477
if query is None: query = ''
478478
if fragment is None: fragment = ''
479479
result = ParseResult(scheme, netloc, url, params, query, fragment)
480+
if netloc and ':' in netloc:
481+
result.port # check that the port is valid
480482
result = _coerce_result(result)
481483
result._keep_empty = missing_as_none
482484
return result
@@ -586,6 +588,8 @@ def urlsplit(url, scheme=None, allow_fragments=True, *, missing_as_none=_MISSING
586588
if query is None: query = ''
587589
if fragment is None: fragment = ''
588590
result = SplitResult(scheme, netloc, url, query, fragment)
591+
if netloc and ':' in netloc:
592+
result.port # check that the port is valid
589593
result = _coerce_result(result)
590594
result._keep_empty = missing_as_none
591595
return result
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:func:`urllib.parse.urlsplit` and :func:`urllib.parse.urlparse` now raise
2+
:exc:`ValueError` for URLs with an invalid port, instead of failing only when
3+
the ``port`` attribute of the result is read. This also rejects URLs with an
4+
unbracketed IPv6 address, like ``"http://::1/"``.

0 commit comments

Comments
 (0)