-
-
Notifications
You must be signed in to change notification settings - Fork 35.3k
bpo-36338: urllib.urlparse rejects invalid IPv6 addresses #16780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -167,13 +167,7 @@ def hostname(self): | |
| def port(self): | ||
| port = self._hostinfo[1] | ||
| if port is not None: | ||
| try: | ||
| port = int(port, 10) | ||
| except ValueError: | ||
| message = f'Port could not be cast to integer value as {port!r}' | ||
| raise ValueError(message) from None | ||
| if not ( 0 <= port <= 65535): | ||
| raise ValueError("Port out of range 0-65535") | ||
| port = _validate_port(port) | ||
| return port | ||
|
|
||
|
|
||
|
|
@@ -388,17 +382,48 @@ def _splitparams(url): | |
| i = url.find(';') | ||
| return url[:i], url[i+1:] | ||
|
|
||
| def _validate_port(port_str): | ||
| try: | ||
| port = int(port_str, 10) | ||
| except ValueError: | ||
| message = f'Port could not be cast to integer value as {port_str!r}' | ||
| raise ValueError(message) from None | ||
| if not (0 <= port <= 65535): | ||
| raise ValueError("Port out of range 0-65535: %r" % port_str) | ||
| return port | ||
|
|
||
| def _check_ipv6_host(host): | ||
| # "[ipv6]" host: ensure that it starts with "[", ends with "]" | ||
| # and that ipv6 doesn't contain "[" nor "]". | ||
| if not(host.startswith('[') and host.endswith(']')): | ||
| return False | ||
|
|
||
| parts = host[1:-1].split('%', 1) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Slightly offtopic but this just reminded me that ipaddress module doesn't support scope id in IPV6 address yet. Maybe once #13772 is merged we can just catch ValidationError and return False since the same validation would already be done in the parser to check for % in scope and remove validation here.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The allowed characters in the scope part is not well defined. I read https://tools.ietf.org/html/rfc4007 and https://tools.ietf.org/html/rfc6874 RFC 6874: A <zone_id> SHOULD contain only ASCII characters classified as If an operating system uses any other characters in zone or interface So... is % allowed in an unquoted URL?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@vstinner - I read the rfc6874 and the part that you quoted. If we want strict adherence to the RFC, I think, allowing % in unquoted URL is correct. I looked at #13772 , which is trying to bring in the scope id to ipaddress module. There is an agreement and a documented statement that states: "If present, the scope ID must be non-empty, and may not contain Let's keep the current change, and not allow '%', assuming that it wont be common for all practical purposes. It will be consistent within the standard library. If the allowance of '%' in scope-id is desired, that could be changed in ipaddress module, and once the ipaddress module scope-id support is merged, we could use the facility here.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sorry but I'm not used to the urllib module. Is urllib.parse.urlsplit() supposed to get a "quoted" or "unquoted" URL?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It is supposed to get the unquoted URL. I relied on tests of urlparse to state this. My reading of RFC 6874 and especially, the part quoted makes me think that 'percent-encoded' character like I lack the experience to say something confidently about 'percent-encoded characters' in zone-id, and i think being consistent within modules and with documentation is the most appropriate thing to do. I am +1 with committing this change. Please let me know you have hesitation or do you want (me/) us to research further.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @orsenthil , @vstinner In #13772 I come to the decision, that
At the same time, Section 2 RFC 6874, especially paragraph 4, presumes, that <zone_id> part may contain That is confusing and seems to conflict with RFC 4007. That's why, I decided to assume, that RFC 6874 conserns only representing IPv6 Zone Identifiers in URI`s. |
||
| ipv6 = parts[0] | ||
| scope = (parts[1] if len(parts) > 1 else None) | ||
|
|
||
| import ipaddress | ||
| try: | ||
| ipaddress.IPv6Address(ipv6) | ||
| except ipaddress.AddressValueError: | ||
| return False | ||
|
|
||
| if scope is not None and any(char in '%[]' for char in scope): | ||
| return False | ||
|
|
||
| return True | ||
|
|
||
| def _splitnetloc(url, start=0): | ||
| delim = len(url) # position of end of domain part of url, default is end | ||
| for c in '/?#': # look for delimiters; the order is NOT important | ||
| wdelim = url.find(c, start) # find first of this delim | ||
| if wdelim >= 0: # if found | ||
| delim = min(delim, wdelim) # use earliest delim position | ||
| return url[start:delim], url[delim:] # return (domain, rest) | ||
| netloc = url[start:delim] | ||
| url = url[delim:] | ||
| return (netloc, url) | ||
|
|
||
| def _checknetloc(netloc): | ||
| if not netloc or netloc.isascii(): | ||
| return | ||
| def _checknetloc_nfkc(netloc): | ||
| # looking for characters like \u2100 that expand to 'a/c' | ||
| # IDNA uses NFKC equivalence, so normalize for this check | ||
| import unicodedata | ||
|
|
@@ -414,6 +439,21 @@ def _checknetloc(netloc): | |
| raise ValueError("netloc '" + netloc + "' contains invalid " + | ||
| "characters under NFKC normalization") | ||
|
|
||
| def _checknetloc(netloc): | ||
| # Optimization: skip the check for empty string and ASCII-only strings. | ||
| # NFKC normalization has no effect on ASCII strings. | ||
| if netloc and not netloc.isascii(): | ||
| _checknetloc_nfkc(netloc) | ||
| host, port = _splitport(netloc) | ||
| # don't validate "user:passwd@" | ||
| user_passwd, host = _splituser(host) | ||
| if '[' in host or ']' in host: | ||
| # "http://[ipv6%scope]:port/path" URL | ||
| if not _check_ipv6_host(host): | ||
| raise ValueError(f"Invalid network location: {netloc!r}") | ||
| if port is not None: | ||
| _validate_port(port) | ||
|
|
||
| def urlsplit(url, scheme='', allow_fragments=True): | ||
| """Parse a URL into 5 components: | ||
| <scheme>://<netloc>/<path>?<query>#<fragment> | ||
|
|
@@ -439,9 +479,6 @@ def urlsplit(url, scheme='', allow_fragments=True): | |
|
|
||
| if url[:2] == '//': | ||
| netloc, url = _splitnetloc(url, 2) | ||
| if (('[' in netloc and ']' not in netloc) or | ||
| (']' in netloc and '[' not in netloc)): | ||
| raise ValueError("Invalid IPv6 URL") | ||
| if allow_fragments and '#' in url: | ||
| url, fragment = url.split('#', 1) | ||
| if '?' in url: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Fix :func:`urllib.parse.urlparse` for IPv6 address when user or password | ||
| contains "[" or "]" character. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| The :mod:`urllib.urlparse` module now rejects invalid IPv6 addresses and | ||
| invalid port numbers when parsing an URL. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This slightly backwards incompatible but I am okay with the intention of the PR in validating port during parsing instead of accessing the port attribute since it just means the URL is invalid and is known earlier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm checking the port to reject [ and ] in the port number. Reject port number outside the [0; 65535] is a side effect. IMHO it's a good thing to reject an invalid URL, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, to be more clear I am fine with the change. It's that previously port validation is done while accessing port attribute allowing invalid URL to be parsed but now it's done in parsing itself which is better as per this PR.