diff --git a/tornado/httpserver.py b/tornado/httpserver.py index 98e0a9eda..8c13fc47c 100644 --- a/tornado/httpserver.py +++ b/tornado/httpserver.py @@ -346,6 +346,10 @@ def _apply_xheaders(self, headers: httputil.HTTPHeaders) -> None: break ip = headers.get("X-Real-Ip", ip) if netutil.is_valid_ip(ip): + # Unwrap the RFC 3986 bracketed IPv6 form so the stored + # remote_ip is always the bare address. + if len(ip) >= 2 and ip[0] == "[" and ip[-1] == "]": + ip = ip[1:-1] self.remote_ip = ip # AWS uses X-Forwarded-Proto proto_header = headers.get( diff --git a/tornado/netutil.py b/tornado/netutil.py index d9e722eff..a22cc2edf 100644 --- a/tornado/netutil.py +++ b/tornado/netutil.py @@ -293,12 +293,18 @@ def remove_handler() -> None: def is_valid_ip(ip: str) -> bool: """Returns ``True`` if the given string is a well-formed IP address. - Supports IPv4 and IPv6. + Supports IPv4 and IPv6. IPv6 addresses wrapped in ``[`` and ``]`` (as + used in URI references per RFC 3986) are also accepted. """ if not ip or "\x00" in ip: # getaddrinfo resolves empty strings to localhost, and truncates # on zero bytes. return False + # Accept the RFC 3986 URI form for IPv6 addresses, e.g. "[::1]". + if len(ip) >= 2 and ip[0] == "[" and ip[-1] == "]": + ip = ip[1:-1] + if not ip: + return False try: res = socket.getaddrinfo( ip, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_NUMERICHOST diff --git a/tornado/test/httpserver_test.py b/tornado/test/httpserver_test.py index 02d4f4c09..e96e005a9 100644 --- a/tornado/test/httpserver_test.py +++ b/tornado/test/httpserver_test.py @@ -698,6 +698,22 @@ def test_ip_headers(self): "2620:0:1cfe:face:b00c::3", ) + # Some load balancers (e.g. Fortigate) emit IPv6 in the RFC 3986 + # bracketed URI form. The bracketed form should be accepted and + # unwrapped to the bare address. + bracketed_ipv6 = {"X-Forwarded-For": "[2620:0:1cfe:face:b00c::3]"} + self.assertEqual( + self.fetch_json("/", headers=bracketed_ipv6)["remote_ip"], + "2620:0:1cfe:face:b00c::3", + ) + bracketed_ipv6_in_list = { + "X-Forwarded-For": "::1, [2620:0:1cfe:face:b00c::3]" + } + self.assertEqual( + self.fetch_json("/", headers=bracketed_ipv6_in_list)["remote_ip"], + "2620:0:1cfe:face:b00c::3", + ) + invalid_chars = {"X-Real-IP": "4.4.4.4