CVE-2024-49767 - Werkzeug / Flask / Quart
2024-10-25 - Python, infosec, CVEWerkzeug is a Web Server Gateway Interface (WSGI)
library used to develop python web applications or frameworks. Applications using
werkzeug.formparser.MultiPartParser to parse multipart/form-data requests (e.g. all
flask and quart applications) were vulnerable to resource exhaustion (denial of
service) attacks. A specifically crafted form submission request could cause the parser to allocate and block
3 to 8 times the upload size in main memory. There was no upper limit; a single upload at 1 Gbit/s could
exhaust 32 GB of RAM in less than 60 seconds.
- CVE-2024-49767
- GHSA-q34m-jh98-gwm2
- CVSSv4: 6.9 (
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N)
Details
The multipart parser implementation found in werkzeug.formparser distinguishes between file
uploads and text fields by looking for a filename option in the Content-Disposition
multipart segment header. If present, the multipart segment is treated as file uploads and buffered into a
SpooledTemporaryFile. If the filename option is missing, the segment is treated as a text field
and buffered into an in-memory list instead. This list is allowed to grow indefinitely. Once complete, the
buffered chunks are concatenated and decoded which causes a peak memory consumption of 3 to 8 times the
original uncompressed upload size. With clever use of specific unicode glyphs this factor can probably be
increased even further.
This allows an attacker to craft multipart/form-data requests that completely exhaust the
servers main memory, causing excessive swapping or out-of-memory situations. A 32GB server can be exhausted
over a 1 Gbit/s line in less than a minute, with just a single request. Transfer-Encoding:
chunked can be used to bypass early safeguards that rely on a Content-Length header.
Sending data slowly over several requests in parallel should avoid timeouts but still block huge amounts of
memory for a long time before individual requests or the entire server process is eventually killed by the
kernels OOM killer.
Other multipart libraries or web frameworks have configurable limits for in-memory (and on-disk) data
structures and will either error out if header sections or text fields grow larger than allowed, or treat
those large text fields as file uploads (with an empty filename) and buffer to disk as a safeguard. In
werkzeug there is no limit for on-disk buffering and the max_form_memory_size limit is only
enforced on the header section of a multipart segment, but ignored for the in-memory list used to buffer text
fields. The max_content_length limit on the other hand is too strict, as it would also limit
regular file uploads. Both limits are not set in Werkzeug, Flask or Quart by default, leaving applications
vulnerable to this attack by default.
Proof of concept
import flask
app = flask.Flask(__name__)
@app.post('/')
def foo():
flask.request.form
return 'DONE'
from quart import Quart, request
app = Quart(__name__)
@app.route('/', methods=["POST"])
async def greet():
await request.form
return 'DONE'
app.run()
$ curl http://localhost:5000 -F 'big=</dev/urandom'
# WARNING: This will fill up memory and start swapping almost instantly. Use --limit-rate 1M for a slow death
Impact
Vulnerable are all applications that use werkzeug.formparser.MultiPartParser to parse
multipart/form-data requests (e.g. all flask and quart applications) and expose an endpoint that
processes form submissions.
Mitigation
Limit maximum upload size in the WSGI/ASGI server or proxy in front of the vulnerable application to a value that would fits into available memory twice per worker thread.
Timeline
03.09.2024 (+0 days) First contact. Sent a detailed report (similar to the one above) to the projects security contact email.
10.09.2024 (+7 days) No response. Contacted their security email again and asked for confirmation. Got a quick response this time. They asked to open a GitHub security advisory, which I did. Note that the text you see there is not the report I submitted.
02.10.2024 (+29 days) No response for three weeks, so I asked again for an estimate. A
maintainer suggested that I should provide a patch myself, mentioned two config setting which were already
shown in the initial report to be ineffective or unsuitable, then continued to argue that 'insecure defaults'
should not count as a security issue in the first place. A bit of back and forth followed, mostly
about the impact and severity of the issue. They then tried to reproduce the issue with
max_content_length = 1000, a value so low that it would also prevent most legitimate requests.
This unusually low value also triggered an unrelated logic bug and broke all form submissions, even
smaller ones. The provided PoC, which uses default values for those settings and worked as expected, was
ignored.
10.10.2024 (+37 days) Mentioned that Starlette had a very similar issue and already released a fix. The now public details are so similar that they can be easily applied to werkzeug. No reaction from the maintainer.
25.10.2024 (+52 days) The same maintainer tried again to reproduce the issue, failed for the same reason that were already discussed 3 weeks ago, and closed the report without waiting for feedback. I explained (again) why a limit below 64k breaks all form submissions, not only malicious ones, and how to properly reproduce the issue. No response.
25.10.2024 (+52 days) I reached out to the public developer discord and asked for a
second opinion without disclosing any details. A couple of hours later, the report was accepted, mods deleted
any mentions from the public discord, the maintainer wrote an incomplete fix, published a heavily edited
report and triggered a security patch release. All this happened within a couple of hours without asking for
feedback or waiting for approval. The fix repaired the max_content_length parameter, but did not
change the insecure defaults. The rewritten report text lacks important details and downplays the impact. It
was published under my name without my consent.
27.10.2024 (+54 days) Opened a public issue mentioning that all Flask and Quart (or other
Werkzeug) applications are still vulnerable by default, and suggesting to introduce reasonable
defaults for critical config settings. This was marked with the label docs instead of
security.
31.10.2024 (+58 days) Werkzeug 3.1.0 was released with secure defaults.
01.10.2024 (+59 days) Noticed and reported that the fix for quart (0.19.7) was not effective.
23.12.2024 (+111 days) Quart was finally fixed, but the report was closed instead of published and no CVE was issued. The original CVE was also not updated, so it still contains inaccurate version ranges.
Conclusion
Communication during this incident left a lot to be desired and the timeline speaks for itself. The next CVE to this or any related projects will be published after a reasonable deadline on independent channels with all details, fixed or not. GitHub security advisories are not suitable for publication if the maintainers actively fight the process.