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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions