python-multipart vulnerable to content-type header Regular expression Denial of Service
Description
python-multipart is a streaming multipart parser for Python. When using form data, python-multipart uses a Regular Expression to parse the HTTP Content-Type header, including options. An attacker could send a custom-made Content-Type option that is very difficult for the RegEx to process, consuming CPU resources and stalling indefinitely (minutes or more) while holding the main event loop. This means that process can't handle any more requests, leading to regular expression denial of service. This vulnerability has been patched in version 0.0.7.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
python-multipartPyPI | < 0.0.7 | 0.0.7 |
Affected products
3- Range: 0
- tiangolo/fastapiv5Range: 0
- encode/starlettev5Range: 0
Patches
313e5c26a27f4Merge pull request from GHSA-93gm-qmq6-w238
1 file changed · +1 −1
pyproject.toml+1 −1 modified@@ -36,7 +36,7 @@ dependencies = [ full = [ "itsdangerous", "jinja2", - "python-multipart", + "python-multipart>=0.0.7", "pyyaml", "httpx>=0.22.0", ]
9d34ad0ee8a0Merge pull request from GHSA-qf9m-vfgh-m389
2 files changed · +2 −2
pyproject.toml+1 −1 modified@@ -56,7 +56,7 @@ Repository = "https://github.com/tiangolo/fastapi" all = [ "httpx >=0.23.0", "jinja2 >=2.11.2", - "python-multipart >=0.0.5", + "python-multipart >=0.0.7", "itsdangerous >=1.1.0", "pyyaml >=5.3.1", "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0",
requirements-tests.txt+1 −1 modified@@ -13,7 +13,7 @@ sqlalchemy >=1.3.18,<1.4.43 databases[sqlite] >=0.3.2,<0.7.0 orjson >=3.2.1,<4.0.0 ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0 -python-multipart >=0.0.5,<0.0.7 +python-multipart >=0.0.7,<0.1.0 flask >=1.1.2,<3.0.0 anyio[trio] >=3.2.1,<4.0.0 python-jose[cryptography] >=3.3.0,<4.0.0
20f0ef6b4e4c♻️ Refactor header option parser to use the standard library instead of a custom RegEx (#75)
1 file changed · +26 −24
multipart/multipart.py+26 −24 modified@@ -9,6 +9,8 @@ import tempfile from io import BytesIO from numbers import Number +from email.message import Message +from typing import Dict, Union, Tuple # Unique missing object. _missing = object() @@ -76,44 +78,44 @@ QUOTE = b'"'[0] -def parse_options_header(value): +def parse_options_header(value: Union[str, bytes]) -> Tuple[bytes, Dict[bytes, bytes]]: """ Parses a Content-Type header into a value in the following format: (content_type, {parameters}) """ + # Uses email.message.Message to parse the header as described in PEP 594. + # Ref: https://peps.python.org/pep-0594/#cgi if not value: return (b'', {}) - # If we are passed a string, we assume that it conforms to WSGI and does - # not contain any code point that's not in latin-1. - if isinstance(value, str): # pragma: no cover - value = value.encode('latin-1') + # If we are passed bytes, we assume that it conforms to WSGI, encoding in latin-1. + if isinstance(value, bytes): # pragma: no cover + value = value.decode('latin-1') + + # For types + assert isinstance(value, str), 'Value should be a string by now' # If we have no options, return the string as-is. - if b';' not in value: - return (value.lower().strip(), {}) + if ";" not in value: + return (value.lower().strip().encode('latin-1'), {}) # Split at the first semicolon, to get our value and then options. - ctype, rest = value.split(b';', 1) + # ctype, rest = value.split(b';', 1) + message = Message() + message['content-type'] = value + params = message.get_params() + # If there were no parameters, this would have already returned above + assert params, 'At least the content type value should be present' + ctype = params.pop(0)[0].encode('latin-1') options = {} - - # Parse the options. - for match in OPTION_RE.finditer(rest): - key = match.group(1).lower() - value = match.group(2) - if value[0] == QUOTE and value[-1] == QUOTE: - # Unquote the value. - value = value[1:-1] - value = value.replace(b'\\\\', b'\\').replace(b'\\"', b'"') - + for param in params: + key, value = param # If the value is a filename, we need to fix a bug on IE6 that sends # the full file path instead of the filename. - if key == b'filename': - if value[1:3] == b':\\' or value[:2] == b'\\\\': - value = value.split(b'\\')[-1] - - options[key] = value - + if key == 'filename': + if value[1:3] == ':\\' or value[:2] == '\\\\': + value = value.split('\\')[-1] + options[key.encode('latin-1')] = value.encode('latin-1') return ctype, options
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
12- github.com/advisories/GHSA-2jv5-9r88-3w3pghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-24762ghsaADVISORY
- github.com/Kludex/python-multipart/commit/20f0ef6b4e4caf7d69a667c54dff57fe467109a4ghsax_refsource_MISCWEB
- github.com/Kludex/python-multipart/security/advisories/GHSA-2jv5-9r88-3w3pghsax_refsource_CONFIRMWEB
- github.com/andrew-d/python-multipart/blob/d3d16dae4b061c34fe9d3c9081d9800c49fc1f7a/multipart/multipart.pyghsax_refsource_MISCWEB
- github.com/encode/starlette/commit/13e5c26a27f4903924624736abd6131b2da80cc5ghsax_refsource_MISCWEB
- github.com/encode/starlette/security/advisories/GHSA-93gm-qmq6-w238mitrex_refsource_MISC
- github.com/github/advisory-database/pull/4829ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/fastapi/PYSEC-2024-38.yamlghsaWEB
- github.com/tiangolo/fastapi/commit/9d34ad0ee8a0dfbbcce06f76c2d5d851085024fcghsax_refsource_MISCWEB
- github.com/tiangolo/fastapi/releases/tag/0.109.1ghsax_refsource_MISCWEB
- github.com/tiangolo/fastapi/security/advisories/GHSA-qf9m-vfgh-m389mitrex_refsource_MISC
News mentions
0No linked articles in our index yet.