Skip to content

Commit

Permalink
fix connection reset when over max content size
Browse files Browse the repository at this point in the history
the current version of werkzeug fails to exhaust the input stream
when it detects that the content exceeds the configured max size.
when run with gunicorn, the stream is never exhausted which leads
to firefox (and others) reporting a connection reset as they
receive the response without the body being uploaded.

here's a sample application:

```python
from flask import Flask, request, redirect, url_for

class Config:
    SECRET_KEY = 'foo'
    MAX_CONTENT_LENGTH = 1024 * 1024 * 1

app = Flask(__name__)
app.config.from_object(Config)

@app.route('/')
def index():
    return '''\
<!doctype html>
<html>
  <head>
    <title>File Upload</title>
  </head>
  <body>
    <h1>File Upload</h1>
    <form method="POST" action="" enctype="multipart/form-data">
      <p><input type="file" name="file"></p>
      <p><input type="submit" value="Submit"></p>
    </form>
  </body>
</html>
'''

@app.route('/', methods=['POST'])
def upload_file():
    request.files['file']
    print('uploaded!')
    return redirect(url_for('index'))
```

when run with gunicorn:

```bash
strace -f gunicorn --bind 127.0.0.1:8080 app:app --access-logfile - 2> log
```

the strace indicates the following happens:

```
[pid 24372] recvfrom(9, "POST / HTTP/1.1\r\nHost: localhost"..., 8192, 0, NULL, NULL) = 8192
...
[pid 24372] sendto(9, "HTTP/1.1 413 REQUEST ENTITY TOO "..., 183, 0, NULL, 0) = 183
[pid 24372] sendto(9, "<!DOCTYPE HTML PUBLIC \"-//W3C//D"..., 196, 0, NULL, 0) = 196
```

in this, werkzeug reads the first 8KB of the request, but then never
any more and starts sending the response

after the fix, werkzeug reads the entire input (and discards it)
before sending a response
  • Loading branch information
asottile committed Feb 27, 2021
1 parent 2fa5818 commit 865b19c
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ Unreleased
- ``request.values`` does not include ``form`` for GET requests (even
though GET bodies are undefined). This prevents bad caching proxies
from caching form data instead of query strings. :pr:`2037`
- Fix connection reset when exceeding max content size. :pr:`2051`


Version 1.0.2
Expand Down
10 changes: 10 additions & 0 deletions src/werkzeug/formparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ def __call__(
...


def _exhaust(stream: t.BinaryIO) -> None:
bts = stream.read(64 * 1024)
while bts:
bts = stream.read(64 * 1024)


def default_stream_factory(
total_content_length: int,
content_type: t.Optional[str],
Expand Down Expand Up @@ -241,6 +247,8 @@ def parse(
and content_length is not None
and content_length > self.max_content_length
):
# if the input stream is not exhausted, firefox reports Connection Reset
_exhaust(stream)
raise exceptions.RequestEntityTooLarge()

if options is None:
Expand Down Expand Up @@ -293,6 +301,8 @@ def _parse_urlencoded(
and content_length is not None
and content_length > self.max_form_memory_size
):
# if the input stream is not exhausted, firefox reports Connection Reset
_exhaust(stream)
raise exceptions.RequestEntityTooLarge()

form = url_decode_stream(stream, self.charset, errors=self.errors, cls=self.cls)
Expand Down
16 changes: 16 additions & 0 deletions tests/test_formparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@ def test_limiting(self):
req.max_content_length = 4
pytest.raises(RequestEntityTooLarge, lambda: req.form["foo"])

# when the request entity is too large, the input stream should be
# drained so that firefox (and others) do not report connection reset
# when run through gunicorn
# a sufficiently large stream is necessary for block-based reads
input_stream = io.BytesIO(b"foo=" + b"x" * 128 * 1024)
req = Request.from_values(
input_stream=input_stream,
content_length=len(data),
content_type="multipart/form-data; boundary=foo",
method="POST",
)
req.max_content_length = 4
pytest.raises(RequestEntityTooLarge, lambda: req.form["foo"])
# ensure that the stream is exhausted
assert input_stream.read() == b""

req = Request.from_values(
input_stream=io.BytesIO(data),
content_length=len(data),
Expand Down

0 comments on commit 865b19c

Please sign in to comment.