[Flask] HTTP/1.1 connection re-use broken in Flask/werkzeug development server?

Daniel Lenski dlenski at gmail.com
Mon May 24 18:27:01 EDT 2021


Hi,
I'm one of the developers of the OpenConnect multi-protocol VPN client.
We've been using Flask to emulate the HTTP-based authentication front-ends
of VPN servers [1], and it's been working really well for this purpose.

I recently discovered that the Flask development server appears to be
unable to correctly handle HTTP/1.1 connection reuse [2]. When a connection
to the development server is reused for multiple requests, the *body* of
one request will be misinterpreted as a new HTTP/1.1 request line.

Here's a complete example, tested against Python 3.6.9, Werkzeug (2.0.1),
and Flask (2.0.1):

    from flask import Flask, abort
    from werkzeug.serving import WSGIRequestHandler
    WSGIRequestHandler.protocol_version = "HTTP/1.1"

    # Create Flask app
    app = Flask(__name__)
    app.config.update(DEBUG=True, PORT=int(port))

    # Add a route that accepts POST
    @app.route('/', methods=('POST',))
    def post_slash():
        assert 'foo' in request.form
        return '', 200

    # Run it
    app.run(host='localhost', port='8080', debug=True)

Demonstration of incorrect parsing of multiple requests by this server,
with curl connection reuse:
     curl -v -X POST http://localhost:8080  -d foo=bar -: -v -X POST
http://localhost:8080 -d foo=baz

Server log shows:
127.0.0.1 - - [24/May/2021 12:28:22] "POST / HTTP/1.1" 200 -
127.0.0.1 - - [24/May/2021 12:28:22] "foo=barPOST / HTTP/1.1" 405 -
127.0.0.1 - - [24/May/2021 12:28:22] code 400, message Bad request syntax
('foo=baz')
127.0.0.1 - - [24/May/2021 12:28:22] "None / HTTP/0.9"
HTTPStatus.BAD_REQUEST -

As mentioned above, the server is evidently misinterpreting the body of the
first request (b"foo=bar" on the wire) as the beginning of the second
request. That's *despite* the fact that the server has clearly *read* the
body and parsed it into the request.form object (otherwise the assertion
would have failed). It appears that the server is not correctly advancing
its position in the connection stream after reading the body, and thus
restarting at the wrong position when awaiting subsequent requests. I also
tested with HTTPS (by setting a valid ssl_context in app.run) and got the
same results.

If the connection is not reused by the client (e.g. two separate curl
commands) then the server handles the second request correctly. It also
succeeds if protocol_version = "HTTP/1.1" is removed from the server
configuration, thus forcing the client *not* to try to reuse connections,
because cause the server identifies itself as HTTP/1.0, which doesn't allow
connection reuse.

Is this a known issue in Flask or Werkzeug? For the time being, it appears
the only workaround is not to reuse HTTP/1.1 connections at all… any easy
fix/patch?

Thanks,
Dan Lenski

[1] See fake-*-server.py in
https://gitlab.com/openconnect/openconnect/tree/master/tests)
[2] https://en.wikipedia.org/wiki/HTTP_persistent_connection#HTTP_1.1
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.python.org/pipermail/flask/attachments/20210524/d7fadb04/attachment.html>


More information about the Flask mailing list