[Django] #37198: content_disposition_header emits invalid header for a filename with a trailing newline ("$" should be "\Z")

3 views
Skip to first unread message

Django

unread,
Jul 1, 2026, 2:39:15 AM (3 days ago) Jul 1
to django-...@googlegroups.com
#37198: content_disposition_header emits invalid header for a filename with a
trailing newline ("$" should be "\Z")
------------------------------+-----------------------------------------
Reporter: Aman Agrawal | Type: Bug
Status: new | Component: HTTP handling
Version: 5.2 | Severity: Normal
Keywords: | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 1 | UI/UX: 0
------------------------------+-----------------------------------------
content_disposition_header() is meant to emit the percent-encoded
filename*=utf-8'' form for any filename that is not a valid RFC 9110
quoted-string, and to only use the bare quoted form for filenames that
are. Its check has a blind spot for a trailing newline:

{{{
>>> from django.utils.http import content_disposition_header
>>> content_disposition_header(True, "report.pdf\n")
'attachment; filename="report.pdf\n"'
>>> content_disposition_header(True, "\n")
'attachment; filename="\n"'

}}}


The returned value contains a raw newline, so setting it as a header
raises BadHeaderError (Django responses), and boto3/http.client raises
ValueError("Invalid header value ...") — an uncaught 500 for anyone
serving a user-supplied filename that ends in a newline. (A newline
*elsewhere* in the filename is handled correctly.)

Root cause
----------
The quotable-character test in django/utils/http.py is:

{{{
quotable_characters = r"^[\t \x21-\x7e]*$"
if is_ascii and re.match(quotable_characters, filename):

}}}


In Python, "$" matches at the end of the string *or immediately before
a trailing "\n"*. So a filename of quotable characters plus one
trailing newline matches, takes the quoted-string branch, and is
emitted verbatim. This is the same class of bug as CVE-2021-32052
(URLValidator "$" accepting a trailing newline), after which validators
were switched to "\Z".

This was introduced with the control-character handling in #36023.

Proposed fix
------------
Anchor with "\Z" (matches only the true end of string):

{{{
- quotable_characters = r"^[\t \x21-\x7e]*$"
+ quotable_characters = r"^[\t \x21-\x7e]*\Z"

}}}


Verified this sends "report.pdf\n" and "\n" to the filename*=utf-8''
branch while leaving all other filenames (e.g. "report.pdf",
"my report.png") on the existing quoted-string branch. A regression
case should be added to ContentDispositionHeaderTests in
tests/utils_tests/test_http.py, e.g.:

{{{
("attachment; filename*=utf-8''report.pdf%0A", True, "report.pdf\n"),
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/37198>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Jul 1, 2026, 11:07:24 AM (3 days ago) Jul 1
to django-...@googlegroups.com
#37198: content_disposition_header emits invalid header for a filename with a
trailing newline ("$" should be "\Z")
-------------------------------+------------------------------------
Reporter: Aman Agrawal | Owner: Vishy
Type: Bug | Status: assigned
Component: HTTP handling | Version: 5.2
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 1 | UI/UX: 0
-------------------------------+------------------------------------
Changes (by Vishy):

* owner: (none) => Vishy
* stage: Unreviewed => Accepted
* status: new => assigned

--
Ticket URL: <https://code.djangoproject.com/ticket/37198#comment:1>
Reply all
Reply to author
Forward
0 new messages