Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES/13206.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fixed ``Content-Disposition`` parsing stripping leading path separators from a
quoted ``filename`` but not from the RFC 5987 ``filename*`` or the RFC 2231
``filename*0``/``filename*0*`` continuation forms, so the sender chose whether
that normalisation applied -- by :user:`arshsmith1`.
6 changes: 3 additions & 3 deletions aiohttp/multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def unescape(text: str, *, chars: str = "".join(map(re.escape, CHAR))) -> str:

elif is_continuous_param(key):
if is_quoted(value):
value = unescape(value[1:-1])
value = unescape(value[1:-1].lstrip("\\/"))
Comment thread
Dreamsorcerer marked this conversation as resolved.
Outdated
elif not is_token(value):
warnings.warn(BadContentDispositionParam(item))
continue
Expand All @@ -150,7 +150,7 @@ def unescape(text: str, *, chars: str = "".join(map(re.escape, CHAR))) -> str:
continue

try:
value = unquote(value, encoding, "strict")
value = unquote(value, encoding, "strict").lstrip("\\/")
except (builtins.LookupError, UnicodeDecodeError):
# The charset is attacker-controlled here; an unknown name
# raises the builtin LookupError (the bare name is shadowed in
Expand Down Expand Up @@ -214,7 +214,7 @@ def content_disposition_filename(
encoding, _, value = value.split("'", 2)
encoding = encoding or "utf-8"
try:
return unquote(value, encoding, "strict")
return unquote(value, encoding, "strict").lstrip("\\/")
except (builtins.LookupError, UnicodeDecodeError):
# Both the charset name and the octets are attacker-controlled
# here; an unknown encoding raises the builtin LookupError
Expand Down
32 changes: 31 additions & 1 deletion tests/test_multipart_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,21 @@ def test_attwithfn2231abspathdisguised(self) -> None:
"attachment; filename*=UTF-8''%5cfoo.html"
)
assert "attachment" == disptype
assert {"filename*": "\\foo.html"} == params
assert {"filename*": "foo.html"} == params

def test_attwithfn2231abspath(self) -> None:
disptype, params = parse_content_disposition(
"attachment; filename*=UTF-8''%2Ffoo.html"
)
assert "attachment" == disptype
assert {"filename*": "foo.html"} == params

def test_attfncontabspath(self) -> None:
disptype, params = parse_content_disposition(
'attachment; filename*0="/foo."; filename*1="html"'
)
assert "attachment" == disptype
assert {"filename*0": "foo.", "filename*1": "html"} == params

def test_attfncont(self) -> None:
disptype, params = parse_content_disposition(
Expand Down Expand Up @@ -711,6 +725,12 @@ def test_filename_ext(self) -> None:
params = {"filename*": "файл.html"}
assert "файл.html" == content_disposition_filename(params)

def test_filename_ext_abspath(self) -> None:
_, params = parse_content_disposition(
'form-data; name="f"; filename="/etc/evil"; filename*=UTF-8\'\'%2Fetc%2Fevil'
)
assert "etc/evil" == content_disposition_filename(params)

def test_attfncont(self) -> None:
params = {"filename*0": "foo.", "filename*1": "html"}
assert "foo.html" == content_disposition_filename(params)
Expand All @@ -723,6 +743,16 @@ def test_attfncontenc(self) -> None:
params = {"filename*0*": "UTF-8''foo-%c3%a4", "filename*1": ".html"}
assert "foo-ä.html" == content_disposition_filename(params)

@pytest.mark.parametrize(
"params",
(
{"filename*0*": "UTF-8''%2Ffoo-%c3%a4", "filename*1": ".html"},
{"filename*0*": "UTF-8''%5cfoo-%c3%a4", "filename*1": ".html"},
),
)
def test_attfncontencabspath(self, params: dict[str, str]) -> None:
assert "foo-ä.html" == content_disposition_filename(params)

@pytest.mark.parametrize(
"params",
(
Expand Down
Loading