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.
- Client posts
https://example.com/form
- Server issues a
303 SEE OTHER
tohttp://example.com/form-posted
- SSLify then issues a
301 PERMANENT REDIRECT
tohttps://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.