38

I just deployed a Flask app on Webfaction and I've noticed that request.remote_addr is always 127.0.0.1. which is of course isn't of much use.

How can I get the real IP address of the user in Flask on Webfaction?

Thanks!

Korrupt
  • 17
  • 4
Ignas Butėnas
  • 6,061
  • 5
  • 32
  • 47

5 Answers5

59

If there is a proxy in front of Flask, then something like this will get the real IP in Flask:

if request.headers.getlist("X-Forwarded-For"):
   ip = request.headers.getlist("X-Forwarded-For")[0]
else:
   ip = request.remote_addr

Update: Very good point mentioned by Eli in his comment. There could be some security issues if you just simply use this. Read Eli's post to get more details.

Community
  • 1
  • 1
Ignas Butėnas
  • 6,061
  • 5
  • 32
  • 47
  • 11
    This is dangerous advice. Webfaction *appends* the real address to X-Forwarded-For so using this code -- which selects the first address in the list -- lets a malicious user spoof their IP address to *any arbitrary string*. See http://esd.io/blog/flask-apps-heroku-real-ip-spoofing.html for longer discussion and my attempt at a solution. – Eli May 13 '13 at 03:49
  • Very good point Eli. Adding the note to the answer with the link to your post. – Ignas Butėnas May 13 '13 at 12:05
  • Not so sure about that. http://docs.webfaction.com/software/django/troubleshooting.html#accessing-remote-addr (it looks like it uses [0], but your point is still correct just not for webfaction I think) – Dexter May 15 '13 at 14:54
  • 12
    You don't want to access `x-forwarded-for` directly. Flask and werkzeug provide [`request.access_route`](http://werkzeug.pocoo.org/docs/wrappers/#werkzeug.wrappers.BaseRequest.access_route) which contains the already-split IP list from that header. – ThiefMaster Aug 10 '13 at 10:51
  • @ThiefMaster: If I apply the Werkzeug ProxyFix then request.access_route will still give me the spoofed IP: http://stackoverflow.com/questions/22868900/how-do-i-safely-get-the-users-real-ip-address-in-flask-using-mod-wsgi – user_78361084 Apr 05 '14 at 00:37
  • Using Nginx, now it gives X-Real-Ip instead of X-Forwarded-For – Sambhav Sharma Feb 04 '16 at 08:19
12

Werkzeug middleware

Flask's documentation is pretty specific about recommended reverse proxy server setup:

If you deploy your application using one of these [WSGI] servers behind an HTTP [reverse] proxy you will need to rewrite a few headers in order for the application to work [properly]. The two problematic values in the WSGI environment usually are REMOTE_ADDR and HTTP_HOST... Werkzeug ships a fixer that will solve some common setups, but you might want to write your own WSGI middleware for specific setups.

And also about security consideration:

Please keep in mind that it is a security issue to use such a middleware in a non-proxy setup because it will blindly trust the incoming headers which might be forged by malicious clients.

The suggested code (that installs the middleware) that will make request.remote_addr return client IP address is:

from werkzeug.contrib.fixers import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, num_proxies=1)

Note num_proxies which is 1 by default. It's the number of proxy servers in front of the app.

The actual code is as follows (lastest werkzeug==0.14.1 at the time of writing):

def get_remote_addr(self, forwarded_for):
    if len(forwarded_for) >= self.num_proxies:
        return forwarded_for[-self.num_proxies]

Webfaction

Webfaction's documentation about Accessing REMOTE_ADDR says:

...the IP address is available as the first IP address in the comma separated list in the HTTP_X_FORWARDED_FOR header.

They don't say what they do when a client request already contains X-Forwarded-For header, but following common sense I would assume they replace it. Thus for Webfaction num_proxies should be set to 0.

Nginx

Nginx is more explicit about it's $proxy_add_x_forwarded_for:

the “X-Forwarded-For” client request header field with the $remote_addr variable appended to it, separated by a comma. If the “X-Forwarded-For” field is not present in the client request header, the $proxy_add_x_forwarded_for variable is equal to the $remote_addr variable.

For Nginx in front of the app num_proxies should be left at default 1.

saaj
  • 23,253
  • 3
  • 104
  • 105
  • 4
    If yuo're running werkzeug 1.0.0 you need to import `from werkzeug.middleware.proxy_fix import ProxyFix` instead. – Fabio Filippi Mar 13 '20 at 16:13
  • The arguments have also changed. Up-to-date documentation can be found here: https://werkzeug.palletsprojects.com/en/2.2.x/middleware/proxy_fix/ – Derkades Feb 22 '23 at 15:43
7

Rewriting the Ignas's answer:

headers_list = request.headers.getlist("X-Forwarded-For")
user_ip = headers_list[0] if headers_list else request.remote_addr

Remember to read Eli's post about spoofing considerations.

ankostis
  • 8,579
  • 3
  • 47
  • 61
Shankar Cabus
  • 9,302
  • 7
  • 33
  • 43
  • Yes the code is more beautiful (mine was more newbie friendly :D) but it still has the same issues mentioned already by the other guys, so be careful. – Ignas Butėnas Sep 04 '13 at 08:09
6

You can use request.access_route to access list of ip :

if len(request.access_route) > 1:
    return request.access_route[-1]
else:
    return request.access_route[0]

Update:

You can just write this:

    return request.access_route[-1]
rezakamalifard
  • 1,289
  • 13
  • 24
  • 2
    You don't actually need the length check here -- `some_list[-1] == some_list[0]` on single length lists –  May 13 '15 at 20:12
  • It's not single length list. it may have more than one item in it. – rezakamalifard May 15 '15 at 10:57
  • 1
    @itmard I think @tristan's point is that the last item is always accessible for lists with >= 1 items. So code can always written as `request.access_route[-1]` to get the last IP in the list. – tutuDajuju Nov 12 '15 at 15:47
  • If you are behind for example proxy server this list has more than one value. this is why this is a list not just a string. – rezakamalifard Nov 16 '15 at 10:11
  • 5
    Useless use of length check, as @tristan pointed out. Your `else` would only match if the length is 1 (Flask will always return at least one result.), which `request.access_route[-1]` would still be valid for. – Liam Stanley Jul 12 '16 at 04:06
4

The problem is there's probably some kind of proxy in front of Flask. In this case the "real" IP address can often be found in request.headers['X-Forwarded-For'].

Rob Wouters
  • 15,797
  • 3
  • 42
  • 36