How do I safely get the user's real IP address in Flask (using mod_wsgi)?

user_78361084 picture user_78361084 · Apr 4, 2014 · Viewed 14.8k times · Source

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

Answer

Martijn Pieters picture Martijn Pieters · Apr 8, 2014

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.