Skip to content
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

Add unsafe_remote_ip property #2975

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
18 changes: 18 additions & 0 deletions tornado/httputil.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from functools import lru_cache
from http.client import responses
import http.cookies
import ipaddress
import re
from ssl import SSLError
import time
Expand All @@ -35,6 +36,7 @@

from tornado.escape import native_str, parse_qs_bytes, utf8
from tornado.log import gen_log
from tornado.netutil import is_valid_ip
from tornado.util import ObjectDict, unicode_type


Expand Down Expand Up @@ -383,6 +385,22 @@ def __init__(
self.query_arguments = copy.deepcopy(self.arguments)
self.body_arguments = {} # type: Dict[str, List[bytes]]

@property
def unsafe_remote_ip(self) -> str:
"""The IP a client claims to be using.

This is the first public IP in the X-Forwarded-For header.

Unlike `remote_ip` this IP is untrustworthy but potentially more
representative of the real IP a client is using. Useful for situations
like geolocation.
"""
ip = self.headers.get("X-Forwarded-For", self.remote_ip)
for ip in (cand.strip() for cand in ip.split(",")):
if is_valid_ip(ip) and ipaddress.ip_address(ip).is_global:
break
return ip

@property
def cookies(self) -> Dict[str, http.cookies.Morsel]:
"""A dictionary of ``http.cookies.Morsel`` objects."""
Expand Down
34 changes: 34 additions & 0 deletions tornado/test/httpserver_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,40 @@ def test_invalid_content_length(self):
yield self.stream.read_until_close()


class UnsafeRemoteIPTest(HandlerBaseTestCase):
class Handler(RequestHandler):
def get(self):
self.set_header("request-version", self.request.version)
self.write(
dict(
remote_ip=self.request.remote_ip,
unsafe_remote_ip=self.request.unsafe_remote_ip,
)
)

def get_httpserver_options(self):
return dict(xheaders=True, trusted_downstream=["5.5.5.5"])

def test_unsafe_ip(self):
self.assertEqual(self.fetch_json("/")["remote_ip"], "127.0.0.1")
self.assertEqual(self.fetch_json("/")["unsafe_remote_ip"], "127.0.0.1")

valid_ip = {"X-Forwarded-for": "4.4.4.4"}
self.assertEqual(
self.fetch_json("/", headers=valid_ip)["unsafe_remote_ip"], "4.4.4.4"
)

valid_ip_list = {"X-Forwarded-for": "3.3.3.3, 4.4.4.4"}
self.assertEqual(
self.fetch_json("/", headers=valid_ip_list)["unsafe_remote_ip"], "3.3.3.3"
)

skip_private_ip = {"X-Forwarded-for": "10.0.0.1, 3.3.3.3, 4.4.4.4"}
self.assertEqual(
self.fetch_json("/", headers=skip_private_ip)["unsafe_remote_ip"], "3.3.3.3"
)


class XHeaderTest(HandlerBaseTestCase):
class Handler(RequestHandler):
def get(self):
Expand Down