Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions tornado/httpserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
8 changes: 7 additions & 1 deletion tornado/netutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions tornado/test/httpserver_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<script>"}
self.assertEqual(
self.fetch_json("/", headers=invalid_chars)["remote_ip"], "127.0.0.1"
Expand Down
9 changes: 9 additions & 0 deletions tornado/test/netutil_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ def test_is_valid_ip(self):
self.assertTrue(is_valid_ip("4.4.4.4"))
self.assertTrue(is_valid_ip("::1"))
self.assertTrue(is_valid_ip("2620:0:1cfe:face:b00c::3"))
# IPv6 in RFC 3986 URI form (e.g. emitted by some load balancers
# in X-Forwarded-For headers, see issue #3561)
self.assertTrue(is_valid_ip("[::1]"))
self.assertTrue(is_valid_ip("[2620:0:d60:ac1a::10]"))
self.assertTrue(is_valid_ip("[2620:0:1cfe:face:b00c::3]"))
self.assertFalse(is_valid_ip("www.google.com"))
self.assertFalse(is_valid_ip("localhost"))
self.assertFalse(is_valid_ip("4.4.4.4<"))
Expand All @@ -186,6 +191,10 @@ def test_is_valid_ip(self):
self.assertFalse(is_valid_ip("\n"))
self.assertFalse(is_valid_ip("\x00"))
self.assertFalse(is_valid_ip("a" * 100))
# Malformed bracketed values are still rejected
self.assertFalse(is_valid_ip("[]"))
self.assertFalse(is_valid_ip("[not-an-ip]"))
self.assertFalse(is_valid_ip("[127.0.0.1"))


class TestPortAllocation(unittest.TestCase):
Expand Down