I have a flask app setup on mod_wsgi/Apache and need to log the IP Address of the user. request.remote_addr returns "127.0.0.1" and this fix attempts to correct that but I've found that Django removed similar code for security reasons.
Is there a better way to safely get the user's real IP address?
EDIT: Maybe I'm missing something obvious. I applied werkzeug's/Flask's fix but it doesn't seem to make a difference when I try a request with altered headers:
run.py:
from werkzeug.contrib.fixers import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)
app.run()
view.py:
for ip in request.access_route:
print ip # prints "1.2.3.4" and "my.ip.address"
This same result happens if I have the ProxyFix enabled or not. I feel like I'm missing something completely obvious
You can use the request.access_route
attribute only if you define a list of trusted proxies.
The access_route
attribute uses the X-Forwarded-For
header, falling back to the REMOTE_ADDR
WSGI variable; the latter is fine as your server determines this; the X-Forwarded-For
could have been set by just about anyone, but if you trust a proxy to set the value correctly, then use the first one (from the end) that is not trusted:
trusted_proxies = {'127.0.0.1'} # define your own set
route = request.access_route + [request.remote_addr]
remote_addr = next((addr for addr in reversed(route)
if addr not in trusted_proxies), request.remote_addr)
That way, even if someone spoofs the X-Forwarded-For
header with fake_ip1,fake_ip2
, the proxy server will add ,spoof_machine_ip
to the end, and the above code will set the remote_addr
to spoof_machine_ip
, no matter how many trusted proxies there are in addition to your outermost proxy.
This is the whitelist approach your linked article talks about (briefly, in that Rails uses it), and what Zope implemented over 11 years ago.
Your ProxyFix approach works just fine, but you misunderstood what it does. It only sets request.remote_addr
; the request.access_route
attribute is unchanged (the X-Forwarded-For
header is not adjusted by the middleware). However, I'd be very wary of blindly counting off proxies.
Applying the same whitelist approach to the middleware would look like:
class WhitelistRemoteAddrFix(object):
"""This middleware can be applied to add HTTP proxy support to an
application that was not designed with HTTP proxies in mind. It
only sets `REMOTE_ADDR` from `X-Forwarded` headers.
Tests proxies against a set of trusted proxies.
The original value of `REMOTE_ADDR` is stored in the WSGI environment
as `werkzeug.whitelist_remoteaddr_fix.orig_remote_addr`.
:param app: the WSGI application
:param trusted_proxies: a set or sequence of proxy ip addresses that can be trusted.
"""
def __init__(self, app, trusted_proxies=()):
self.app = app
self.trusted_proxies = frozenset(trusted_proxies)
def get_remote_addr(self, remote_addr, forwarded_for):
"""Selects the new remote addr from the given list of ips in
X-Forwarded-For. Picks first non-trusted ip address.
"""
if remote_addr in self.trusted_proxies:
return next((ip for ip in reversed(forwarded_for)
if ip not in self.trusted_proxies),
remote_addr)
def __call__(self, environ, start_response):
getter = environ.get
remote_addr = getter('REMOTE_ADDR')
forwarded_for = getter('HTTP_X_FORWARDED_FOR', '').split(',')
environ.update({
'werkzeug.whitelist_remoteaddr_fix.orig_remote_addr': remote_addr,
})
forwarded_for = [x for x in [x.strip() for x in forwarded_for] if x]
remote_addr = self.get_remote_addr(remote_addr, forwarded_for)
if remote_addr is not None:
environ['REMOTE_ADDR'] = remote_addr
return self.app(environ, start_response)
To be explicit: this middleware too, only sets request.remote_addr
; request.access_route
remains unaffected.