Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make Flask's url_for use the 'https' scheme in an AWS load balancer without messing with SSLify

I've recently added a SSL certificate to my webapp. It's deployed on Amazon Web Services uses load balancers. The load balancers work as reverse proxies, handling external HTTPS and sending internal HTTP. So all traffic to my Flask app is HTTP, not HTTPS, despite being a secure connection.

Because the site was already online before the HTTPS migration, I used SSLify to send 301 PERMANENT REDIRECTS to HTTP connections. It works despite all connections being HTTP because the reverse proxy sets the X-Forwarded-Proto request header with the original protocol.

The problem

url_for doesn't care about X-Forwarded-Proto. It will use the my_flask_app.config['PREFERRED_URL_SCHEME'] when a scheme isn't available, but during a request a scheme is available. The HTTP scheme of the connection with the reverse proxy.

So when someone connects to https://example.com, it connects to the load balancer, which then connects to Flask using http://example.com. Flask sees the http and assumes the scheme is HTTP, not HTTPS as it originally was.

That isn't a problem in most url_for used in templates, but any url_for with _external=True will use http instead of https. Personally, I use _external=True for rel=canonical since I heard it was recommended practice. Besides that, using Flask.redirect will prepend non-_external urls with http://example.com, since the redirect header must be a fully qualified URL.

If you redirect on a form post for example, this is what would happen.

  1. Client posts https://example.com/form
  2. Server issues a 303 SEE OTHER to http://example.com/form-posted
  3. SSLify then issues a 301 PERMANENT REDIRECT to https://example.com/form-posted

Every redirect becomes 2 redirects because of SSLify.

Attempted solutions

Adding PREFERRED_URL_SCHEME config

https://stackoverflow.com/a/26636880/1660459

my_flask_app.config['PREFERRED_URL_SCHEME'] = 'https' 

Doesn't work because there is a scheme during a request, and that one is used instead. See https://github.com/mitsuhiko/flask/issues/1129#issuecomment-51759359

Wrapping a middleware to mock HTTPS

https://stackoverflow.com/a/28247577/1660459

def _force_https(app):     def wrapper(environ, start_response):         environ['wsgi.url_scheme'] = 'https'         return app(environ, start_response)     return wrapper app = Flask(...) app = _force_https(app) 

As is, this didn't work because I needed that app later. So I used wsgi_app instead.

def _force_https(wsgi_app):     def wrapper(environ, start_response):         environ['wsgi.url_scheme'] = 'https'         return wsgi_app(environ, start_response)     return wrapper app = Flask(...) app.wsgi_app = _force_https(app.wsgi_app) 

Because wsgi_app is called before any app.before_request handlers, doing this makes SSLify think the app is already behind a secure request and then it won't do any HTTP-to-HTTPS redirects.

Patching url_for

(I can't even find where I got this one from)

from functools import partial import Flask Flask.url_for = partial(Flask.url_for, _scheme='https') 

This could work, but Flask will give an error if you set _scheme but not _external. Since most of my app url_for are internal, it doesn't work at all.

like image 616
OdraEncoded Avatar asked Jan 15 '16 00:01

OdraEncoded


People also ask

What does flask's url_for () do?

Flask url_for is defined as a function that enables developers to build and generate URLs on a Flask application. As a best practice, it is the url_for function that is required to be used, as hard coding the URL in templates and view function of the Flask application tends to utilize more time during modification.

What is url_for?

The url_for() function generates the URL to a view based on a name and arguments. The name associated with a view is also called the endpoint, and by default it's the same as the name of the view function.


1 Answers

I was having these same issues with `redirect(url_for('URL'))' behind an AWS Elastic Load Balancer recently & I solved it this using the werkzeug.contrib.fixers.ProxyFix call in my code. example:

from werkzeug.contrib.fixers import ProxyFix app = Flask(__name__)  app.wsgi_app = ProxyFix(app.wsgi_app) 

The ProxyFix(app.wsgi_app) adds HTTP proxy support to an application that was not designed with HTTP proxies in mind. It sets REMOTE_ADDR, HTTP_HOST from X-Forwarded headers.

Example:

from werkzeug.middleware.proxy_fix import ProxyFix # App is behind one proxy that sets the -For and -Host headers. app = ProxyFix(app, x_for=1, x_host=1) 
like image 192
punkdata Avatar answered Sep 21 '22 05:09

punkdata