Opened 6 weeks ago

Closed 3 weeks ago

#37198 closed Bug (fixed)

content_disposition_header emits invalid header for a filename with a trailing newline ("$" should be "\Z")

Reported by: Aman Agrawal Owned by: Vishy
Component: HTTP handling Version: 5.2
Severity: Normal Keywords:
Cc: Aman Agrawal Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: yes UI/UX: no

Description (last modified by Sarah Boyce)

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"),

Change History (8)

comment:1 by Vishy, 6 weeks ago

Owner: set to Vishy
Status: newassigned
Triage Stage: UnreviewedAccepted

comment:2 by Vishy, 6 weeks ago

Has patch: set

Should this be considered a security vulnerability?

comment:3 by Vishy, 6 weeks ago

PR: https://github.com/django/django/pull/21583

Fix is implemented in this PR.

Last edited 6 weeks ago by Vishy (previous) (diff)

comment:4 by blighj, 4 weeks ago

Triage Stage: AcceptedReady for checkin

This seems ready for checkin to me.

One thing I'd correct on the original ticket:

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

It wasn't so much introduced, as that fix missed this edge case. It wasn't working before that ticket, \n anywhere in the filename wasn't working at that point.

Version 0, edited 4 weeks ago by blighj (next)

in reply to:  2 comment:5 by Jacob Walls, 4 weeks ago

Replying to Vishy:

Should this be considered a security vulnerability?

Potential security issues should never be reported on the public bug tracker, as explained on the new ticket form. Send reports instead to the security email address.

Since the report says this leads to a 500, I don't see a security impact (that is, I see no way to successfully spoof headers).

comment:6 by Vishy, 3 weeks ago

Thanks, Jacob, for the clarification. I wasn't able to come up with a successful way to manipulate the header either, so I assumed this wouldn't constitute a security issue.

comment:7 by Sarah Boyce, 3 weeks ago

Description: modified (diff)

comment:8 by Sarah Boyce <42296566+sarahboyce@…>, 3 weeks ago

Resolution: fixed
Status: assignedclosed

In c2517faf:

Fixed #37198 -- Fixed content_disposition_header() quoting of filenames ending with newlines.

Note: See TracTickets for help on using tickets.
Back to Top