Safeguard
Vulnerability Analysis

Werkzeug multipart parser DoS (CVE-2023-46136)

A crafted multipart upload could pin Werkzeug workers at 100% CPU with no auth required. Here's what CVE-2023-46136 affects and how to fix it.

Vikram Iyer
Cloud Security Engineer
7 min read

Werkzeug, the WSGI toolkit that underpins Flask and a long list of downstream Python web frameworks, shipped a multipart form-data parser that could be knocked over with a single crafted HTTP request. Tracked as CVE-2023-46136 (GitHub Security Advisory GHSA-hrfv-mqp8-q5rw), the flaw lets an attacker submit a file upload whose first bytes are a carriage return or line feed character followed by megabytes of data containing none of the delimiter characters the parser is searching for. Because Werkzeug's multipart implementation accumulated the entire upload into a single growing buffer and re-scanned that buffer from the start on every chunk, a single request could pin a worker process at 100% CPU or exhaust available memory — a classic denial-of-service (DoS) primitive delivered over a completely unauthenticated, standard-looking form submission. For any Flask, Quart, or other Werkzeug-based service that accepts file uploads or ordinary multipart forms from the internet, this is a low-effort way to degrade or take down application workers.

What actually breaks

Werkzeug's formparser module parses multipart/form-data bodies by locating boundary markers to split the request into individual parts. The vulnerable code path handled a part beginning with \r or \n inefficiently: instead of discarding the leading line-ending bytes and resuming a bounded search, it appended incoming data chunk-by-chunk into an internal bytearray and re-ran the boundary search over the entire accumulated buffer each time. For a legitimate small upload this is unnoticeable. For an attacker who sends a part that starts with a stray CR/LF and is otherwise several megabytes of boundary-free filler, the repeated re-scanning becomes effectively quadratic in the size of the payload. The result is exactly the CWE-400 (Uncontrolled Resource Consumption) pattern: attacker-controlled input directly drives unbounded CPU and memory work on the server side, with no authentication and minimal request complexity required.

The practical impact scales with concurrency. A single request can tie up one worker; a modest number of parallel requests sent continuously can exhaust an entire worker pool, since threads or processes get stuck in the parsing loop rather than crashing outright — meaning the condition can persist and starve legitimate traffic for as long as the attacker keeps sending requests.

Affected versions and components

  • Werkzeug 2.0.0rc1 through 2.3.7 — vulnerable.
  • Werkzeug 3.0.0 — vulnerable (the 3.x line introduced the bug on that release before it was fixed).
  • Fixed in Werkzeug 2.3.8 and 3.0.1.

Because Werkzeug is a foundational dependency rather than an application-facing product, exposure isn't limited to teams who deliberately chose it — it is pulled in transitively by Flask, Flask-based frameworks, and any project that reuses Werkzeug's parse_form_data/FormDataParser for multipart handling. Any service that:

  • runs Flask, Quart, or another Werkzeug-backed WSGI app,
  • accepts multipart/form-data requests (file uploads, form submissions with file fields, some API endpoints that reuse form parsing for convenience), and
  • is reachable by untrusted or semi-trusted clients

is a candidate for exploitation. This includes internal admin panels, partner-facing upload endpoints, and SaaS products that expose file-upload functionality — none of which necessarily look like "attack surface" in a casual dependency review, which is part of why this class of vulnerability tends to linger in production long after a fix is available.

CVSS, EPSS, and KEV context

Scoring for CVE-2023-46136 varies slightly by source, which is worth calling out for teams trying to triage it correctly:

  • GitHub Advisory / NVD-aligned score: CVSS v3.1 5.7 (Medium), vector AV:A/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H.
  • Alternate scoring seen in some downstream advisories (e.g., certain vendor bulletins): CVSS v3.1 7.5 (High), vector AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.

The discrepancy largely comes down to how different scorers characterize attack vector and privileges required for a given deployment — the network-facing, unauthenticated 7.5 characterization is the one that matches most public-facing Flask/Werkzeug deployments, and is the more defensible number to use for prioritization if your affected service sits behind no auth on the vulnerable upload path. Either way, the impact metric is unambiguous: Availability: High, with no confidentiality or integrity impact — this is a pure DoS bug, not a path to data exposure or code execution.

EPSS: at time of writing, CVE-2023-46136 carries an EPSS score around 1.07% (roughly the 61st percentile) — modest in absolute terms, but not negligible for a vulnerability with such a low bar to exploit (a single crafted request, no authentication, no tooling beyond a HTTP client).

KEV: CVE-2023-46136 is not currently listed in CISA's Known Exploited Vulnerabilities catalog. Absence from KEV should not be read as "safe to deprioritize," though — DoS bugs against widely-used web frameworks are exactly the kind of low-noise, high-availability-impact issue that doesn't always generate the kind of incident reporting that drives KEV inclusion, even when it's actively used against smaller or less-monitored targets.

Timeline

  • Werkzeug 2.0.0rc1 (2020) — vulnerable multipart parsing logic present in the initial implementation.
  • Werkzeug 3.0.0 — vulnerability reintroduced/present on the 3.x line at initial release.
  • October 24, 2023 — Werkzeug 2.3.8 and 3.0.1 released, fixing the boundary-search inefficiency; GitHub Security Advisory GHSA-hrfv-mqp8-q5rw published.
  • October 25, 2023 — CVE-2023-46136 formally registered.
  • November 2023 — downstream vendors (NetApp, IBM, and others shipping Werkzeug-based products such as Cloud Pak for Data, Storage Ceph, Maximo Application Suite, and Process Mining) began publishing their own security bulletins referencing the CVE as it was picked up by SBOM and dependency scanning in their release pipelines.

The multi-year gap between the bug's introduction and its fix is a useful reminder that resource-exhaustion bugs in parsing code are easy to miss in code review — they don't fail functionally on typical inputs, they only misbehave under adversarial ones.

Remediation

  1. Upgrade Werkzeug immediately. Move to 2.3.8 or later on the 2.x line, or 3.0.1 or later on the 3.x line. If you depend on Flask, confirm your Flask version pulls in a patched Werkzeug — check pip show werkzeug or your lockfile directly rather than assuming Flask's pinned range is current.
  2. Pin and verify the fix in your lockfile. Update requirements.txt, poetry.lock, Pipfile.lock, or equivalent, and re-run your dependency resolution so transitive pulls of Werkzeug (via Flask, Quart, or other frameworks) also land on a patched version — a direct pin on your own app doesn't help if a vendored or bundled component still ships the vulnerable release.
  3. Rebuild and redeploy container images. If Werkzeug is baked into a base image or vendored into a container layer, a pip install --upgrade in a running environment won't fix it — you need a new image build and a redeploy across every environment running the affected service.
  4. Add request-size and time limits in front of the application. Regardless of the underlying library fix, defense-in-depth matters here: enforce Content-Length caps, request body size limits, and per-request timeout thresholds at your reverse proxy or WAF (nginx, Envoy, cloud load balancer) so a single oversized or slow-parsing request can't monopolize a worker indefinitely.
  5. Rate-limit and monitor upload endpoints. Apply per-IP or per-account rate limiting to endpoints that accept multipart/form-data, and alert on anomalous CPU or memory spikes correlated with upload traffic — this catches both this specific CVE and future, as-yet-unknown parsing DoS bugs in the same code paths.
  6. Audit for indirect exposure. Search your dependency tree — not just your direct requirements.txt — for Werkzeug, since it is frequently pulled in transitively by testing tools, admin dashboards, and internal tooling that don't show up in a cursory review of production dependencies.

How Safeguard Helps

Safeguard's SBOM generation and ingest pipeline surfaces every instance of Werkzeug across your services — including versions buried in transitive dependencies, container base images, and internal tooling that a manual audit would miss — so you get a complete, accurate inventory of exactly where CVE-2023-46136 is present. Reachability analysis then determines whether your application code actually invokes the vulnerable multipart parsing path (versus, say, only using Werkzeug's routing utilities), letting your team separate "must patch now" from "patch on the normal cycle" instead of treating every hit as equally urgent. Griffin AI correlates that reachability signal with each service's actual internet exposure and the CVSS/EPSS context above to produce a prioritized, risk-ranked queue rather than a flat CVE list. When a fix is warranted, Safeguard can open an auto-fix pull request that bumps Werkzeug to a patched release and updates your lockfile in place, so remediation moves from "ticket in a backlog" to "PR ready for review" in minutes rather than sprints.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.