fix(server): use RFC 2183 backslash escaping for quotes in filenames

Previous approach replaced " with ' which corrupts the actual filename.
RFC 2183 specifies that within a quoted-string, backslash is the escape
character. Now properly escapes both \ and " with backslash prefix.

The UTF-8 filename* parameter uses percent-encoding which correctly
preserves the original filename for modern clients.
This commit is contained in:
RUiNtheExtinct 2025-12-28 14:43:19 +05:30
parent 99624cd2eb
commit 40f6073862
2 changed files with 17 additions and 8 deletions

View File

@ -200,8 +200,8 @@ def create_content_disposition_header(filename: str) -> str:
""" """
# ASCII-safe filename for legacy clients (replace non-ASCII with ?) # ASCII-safe filename for legacy clients (replace non-ASCII with ?)
ascii_filename = filename.encode('ascii', 'replace').decode('ascii') ascii_filename = filename.encode('ascii', 'replace').decode('ascii')
# Escape quotes to prevent malformed header (e.g., filename="file"name.png") # RFC 2183: escape backslashes and quotes within quoted-string
ascii_filename = ascii_filename.replace('"', "'") ascii_filename = ascii_filename.replace('\\', '\\\\').replace('"', '\\"')
# RFC 5987 percent-encoded filename for UTF-8 support # RFC 5987 percent-encoded filename for UTF-8 support
encoded_filename = quote(filename, safe='') encoded_filename = quote(filename, safe='')
return f"attachment; filename=\"{ascii_filename}\"; filename*=UTF-8''{encoded_filename}" return f"attachment; filename=\"{ascii_filename}\"; filename*=UTF-8''{encoded_filename}"

View File

@ -15,8 +15,8 @@ def create_content_disposition_header(filename: str) -> str:
""" """
# ASCII-safe filename for legacy clients (replace non-ASCII with ?) # ASCII-safe filename for legacy clients (replace non-ASCII with ?)
ascii_filename = filename.encode('ascii', 'replace').decode('ascii') ascii_filename = filename.encode('ascii', 'replace').decode('ascii')
# Escape quotes to prevent malformed header (e.g., filename="file"name.png") # RFC 2183: escape backslashes and quotes within quoted-string
ascii_filename = ascii_filename.replace('"', "'") ascii_filename = ascii_filename.replace('\\', '\\\\').replace('"', '\\"')
# RFC 5987 percent-encoded filename for UTF-8 support # RFC 5987 percent-encoded filename for UTF-8 support
encoded_filename = quote(filename, safe='') encoded_filename = quote(filename, safe='')
return f"attachment; filename=\"{ascii_filename}\"; filename*=UTF-8''{encoded_filename}" return f"attachment; filename=\"{ascii_filename}\"; filename*=UTF-8''{encoded_filename}"
@ -81,16 +81,25 @@ class TestContentDispositionHeader:
assert 'filename=""' in result assert 'filename=""' in result
def test_filename_with_quotes(self): def test_filename_with_quotes(self):
"""Test that quotes in filename don't break the header.""" """Test that quotes in filename are properly escaped per RFC 2183."""
# Quotes in filenames must be escaped to prevent malformed headers
result = create_content_disposition_header('file"name.png') result = create_content_disposition_header('file"name.png')
assert result.startswith("attachment;") assert result.startswith("attachment;")
# Quotes should be replaced with single quotes in ASCII fallback # RFC 2183: quotes must be backslash-escaped within quoted-string
assert "filename=\"file'name.png\"" in result assert 'filename="file\\"name.png"' in result
# UTF-8 version should have percent-encoded quote # UTF-8 version should have percent-encoded quote
assert "filename*=UTF-8''file%22name.png" in result assert "filename*=UTF-8''file%22name.png" in result
def test_filename_with_backslash(self):
"""Test that backslashes in filename are properly escaped per RFC 2183."""
result = create_content_disposition_header('file\\name.png')
assert result.startswith("attachment;")
# RFC 2183: backslashes must be escaped as \\
assert 'filename="file\\\\name.png"' in result
# UTF-8 version should have percent-encoded backslash
assert "filename*=UTF-8''file%5Cname.png" in result
def test_mixed_ascii_unicode(self): def test_mixed_ascii_unicode(self):
"""Test filenames with both ASCII and Unicode characters.""" """Test filenames with both ASCII and Unicode characters."""
result = create_content_disposition_header("photo_照片_2024.png") result = create_content_disposition_header("photo_照片_2024.png")