diff --git a/Doc/library/urllib.parse.rst b/Doc/library/urllib.parse.rst index 26e159306c81070..4425c45cdcdd300 100644 --- a/Doc/library/urllib.parse.rst +++ b/Doc/library/urllib.parse.rst @@ -162,8 +162,9 @@ or on combining URL components into a URL string. .. [1] Depending on the value of the *missing_as_none* argument. - Reading the :attr:`port` attribute will raise a :exc:`ValueError` if - an invalid port is specified in the URL. See section + An invalid port specified in the URL will raise a :exc:`ValueError`. + Reading the :attr:`port` attribute of a result object created directly + will raise a :exc:`ValueError` too. See section :ref:`urlparse-result-object` for more information on the result object. Unmatched square brackets in the :attr:`netloc` attribute will raise a @@ -225,6 +226,11 @@ or on combining URL components into a URL string. .. versionchanged:: 3.15 Added the *missing_as_none* parameter. + .. versionchanged:: next + An invalid port now raises :exc:`ValueError` when the URL is parsed, + not only when the :attr:`~urllib.parse.SplitResult.port` attribute is + read. + .. _WHATWG spec: https://url.spec.whatwg.org/#concept-basic-url-parser diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index 1e5f79998e7cab2..3fdc8f3c88b1bf2 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -400,12 +400,16 @@ def test_url_host_with_newline_header_injection_rejected(self): host = "localhost\r\nX-injected: header\r\n" schemeless_url = "//" + host + ":8080/test/?test=a" try: - InvalidURL = http.client.InvalidURL - with self.assertRaisesRegex( - InvalidURL, r"contain control.*\\r"): + # The URL is rejected when it is parsed, because the injected + # header is not a valid port. + with self.assertRaises(ValueError): urllib.request.urlopen(f"http:{schemeless_url}") - with self.assertRaisesRegex(InvalidURL, r"contain control.*\\n"): + with self.assertRaises(ValueError): urllib.request.urlopen(f"https:{schemeless_url}") + # Such host is rejected by http.client as well. + with self.assertRaisesRegex(http.client.InvalidURL, + r"contain control.*\\r"): + http.client.HTTPConnection(host, 8080) finally: self.unfakehttp() diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py index a5b7966c7780e9e..a27c51dbca6051b 100644 --- a/Lib/test/test_urlparse.py +++ b/Lib/test/test_urlparse.py @@ -917,9 +917,8 @@ def test_urlsplit_attributes(self): # Verify an illegal port raises ValueError url = b"HTTP://WWW.PYTHON.ORG:65536/doc/#frag" - p = urllib.parse.urlsplit(url) with self.assertRaisesRegex(ValueError, "out of range"): - p.port + urllib.parse.urlsplit(url) def test_urlsplit_remove_unsafe_bytes(self): # Remove ASCII tabs and newlines from input @@ -1029,11 +1028,32 @@ def test_attributes_bad_port(self, bytes, parse, port): self.skipTest('non-ASCII bytes') netloc = str_encode(netloc) url = str_encode(url) - p = parse(url) - self.assertEqual(p.netloc, netloc) + with self.assertRaises(ValueError): + parse(url) + # The port is still checked when it is read from a result + # constructed directly. + if bytes: + p = urllib.parse.SplitResultBytes(b'http', netloc, b'/', b'', b'') + else: + p = urllib.parse.SplitResult('http', netloc, '/', '', '') with self.assertRaises(ValueError): p.port + @support.subTests('parse', (urllib.parse.urlsplit, urllib.parse.urlparse)) + @support.subTests('netloc', ("::1", "a:b:c", "user@::1", "[::1]:80:80")) + def test_attributes_bad_netloc_port(self, parse, netloc): + """Check handling of a colon which does not delimit a valid port.""" + with self.assertRaises(ValueError): + parse("http://" + netloc + "/") + + @support.subTests('parse', (urllib.parse.urlsplit, urllib.parse.urlparse)) + @support.subTests('netloc', ("www.example.net", "www.example.net:", + "user:password@www.example.net", + "[::1]", "[::1]:80", "[::1]:")) + def test_attributes_good_port(self, parse, netloc): + """Check that valid netlocs are not rejected.""" + self.assertEqual(parse("http://" + netloc + "/").netloc, netloc) + @support.subTests('bytes', (False, True)) @support.subTests('parse', (urllib.parse.urlsplit, urllib.parse.urlparse)) @support.subTests('scheme', (".", "+", "-", "0", "http&", "६http")) @@ -1670,13 +1690,11 @@ def test_splitting_bracketed_hosts(self): def test_port_casting_failure_message(self): message = "Port could not be cast to integer value as 'oracle'" - p1 = urllib.parse.urlparse('http://Server=sde; Service=sde:oracle') with self.assertRaisesRegex(ValueError, message): - p1.port + urllib.parse.urlparse('http://Server=sde; Service=sde:oracle') - p2 = urllib.parse.urlsplit('http://Server=sde; Service=sde:oracle') with self.assertRaisesRegex(ValueError, message): - p2.port + urllib.parse.urlsplit('http://Server=sde; Service=sde:oracle') def test_telurl_params(self): p1 = urllib.parse.urlparse('tel:123-4;phone-context=+1-650-516') diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py index 4247b9a4b07fa3f..f76d84473b90051 100644 --- a/Lib/urllib/parse.py +++ b/Lib/urllib/parse.py @@ -477,6 +477,8 @@ def urlparse(url, scheme=None, allow_fragments=True, *, missing_as_none=_MISSING if query is None: query = '' if fragment is None: fragment = '' result = ParseResult(scheme, netloc, url, params, query, fragment) + if netloc and ':' in netloc: + result.port # check that the port is valid result = _coerce_result(result) result._keep_empty = missing_as_none return result @@ -586,6 +588,8 @@ def urlsplit(url, scheme=None, allow_fragments=True, *, missing_as_none=_MISSING if query is None: query = '' if fragment is None: fragment = '' result = SplitResult(scheme, netloc, url, query, fragment) + if netloc and ':' in netloc: + result.port # check that the port is valid result = _coerce_result(result) result._keep_empty = missing_as_none return result diff --git a/Misc/NEWS.d/next/Library/2026-08-05-11-20-00.gh-issue-64470.uRLprt.rst b/Misc/NEWS.d/next/Library/2026-08-05-11-20-00.gh-issue-64470.uRLprt.rst new file mode 100644 index 000000000000000..065901ddec98071 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-05-11-20-00.gh-issue-64470.uRLprt.rst @@ -0,0 +1,4 @@ +:func:`urllib.parse.urlsplit` and :func:`urllib.parse.urlparse` now raise +:exc:`ValueError` for URLs 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/"``.