Describe the bug
aiohttp.multipart.content_disposition_filename() reassembles RFC 2231 parameter continuations (filename*0, filename*1, …) using a lexicographic sorted() over the parameter names. String ordering puts filename*10 between filename*1 and filename*2, and the sequential-index check then stops at the first mismatch, so any filename split into 10 or more sections is silently truncated to its first two sections. RFC 2231 Section 3 requires the sections to be processed in numeric order.
Relatedly, decoding is applied to the joined string based on whether it contains an apostrophe, rather than per-section per RFC 2231 Section 4.1 — so a quoted section containing a single apostrophe raises an uncaught ValueError, and two apostrophes are misread as a charset'language' prefix, corrupting the name.
To Reproduce
from aiohttp.multipart import content_disposition_filename, parse_content_disposition
header = "attachment; " + "; ".join(f'filename*{i}="seg{i}-"' for i in range(11))
_, params = parse_content_disposition(header)
print(content_disposition_filename(params)) # seg0-seg1-
content_disposition_filename({"filename*0": "it's", "filename*1": ".html"}) # ValueError
Expected behavior
seg0-seg1-seg2-seg3-seg4-seg5-seg6-seg7-seg8-seg9-seg10-; and it's.html for the second call. Sections should be ordered numerically and each section decoded according to its own *N* (percent-encoded) / *N (literal) marker.
aiohttp Version
Verified on master (4.0.0a2.dev0, commit 8c8906a); the sorted() logic long predates the current release lines, so 3.x is likely affected as well (not tested there).
Root cause
content_disposition_filename() in aiohttp/multipart.py — sorted() over (key, value) string tuples plus an all-or-nothing "'" in joined_value decode heuristic.
Related component
Multipart reader (BodyPartReader.filename/.name and content-disposition parsing route through this helper).
I have a fix ready and will open a PR.
Describe the bug
aiohttp.multipart.content_disposition_filename()reassembles RFC 2231 parameter continuations (filename*0,filename*1, …) using a lexicographicsorted()over the parameter names. String ordering putsfilename*10betweenfilename*1andfilename*2, and the sequential-index check then stops at the first mismatch, so any filename split into 10 or more sections is silently truncated to its first two sections. RFC 2231 Section 3 requires the sections to be processed in numeric order.Relatedly, decoding is applied to the joined string based on whether it contains an apostrophe, rather than per-section per RFC 2231 Section 4.1 — so a quoted section containing a single apostrophe raises an uncaught
ValueError, and two apostrophes are misread as acharset'language'prefix, corrupting the name.To Reproduce
Expected behavior
seg0-seg1-seg2-seg3-seg4-seg5-seg6-seg7-seg8-seg9-seg10-; andit's.htmlfor the second call. Sections should be ordered numerically and each section decoded according to its own*N*(percent-encoded) /*N(literal) marker.aiohttp Version
Verified on master (4.0.0a2.dev0, commit 8c8906a); the
sorted()logic long predates the current release lines, so 3.x is likely affected as well (not tested there).Root cause
content_disposition_filename()inaiohttp/multipart.py—sorted()over(key, value)string tuples plus an all-or-nothing"'" in joined_valuedecode heuristic.Related component
Multipart reader (
BodyPartReader.filename/.nameand content-disposition parsing route through this helper).I have a fix ready and will open a PR.