Skip to content

content_disposition_filename() truncates filenames split into 10+ RFC 2231 continuation sections (lexicographic sort) #13499

Description

@2sumtech

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.pysorted() 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions