Skip to content

Commit

Permalink
treat multipart as bytes, not str. fixes #5148 (#5917)
Browse files Browse the repository at this point in the history
  • Loading branch information
mhils committed Feb 7, 2023
1 parent 430833e commit 7da3a8e
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 35 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
* Updating `Request.port` now also updates the Host header if present.
This aligns with `Request.host`, which already does this.
([#5908](https://github.com/mitmproxy/mitmproxy/pull/5908), @sujaldev)
* Fix editing of multipart HTTP requests from the CLI.
([#5148](https://github.com/mitmproxy/mitmproxy/issues/5148), @mhils)

### Breaking Changes

Expand Down
2 changes: 1 addition & 1 deletion mitmproxy/contentviews/multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def _format(v):
def __call__(self, data: bytes, content_type: Optional[str] = None, **metadata):
if content_type is None:
return
v = multipart.decode(content_type, data)
v = multipart.decode_multipart(content_type, data)
if v:
return "Multipart form", self._format(v)

Expand Down
28 changes: 15 additions & 13 deletions mitmproxy/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from collections.abc import Iterable
from collections.abc import Iterator
from collections.abc import Mapping
from collections.abc import Sequence
from dataclasses import dataclass
from dataclasses import fields
from email.utils import formatdate
Expand Down Expand Up @@ -963,7 +964,7 @@ def _get_urlencoded_form(self):
return tuple(url.decode(self.get_text(strict=False)))
return ()

def _set_urlencoded_form(self, form_data):
def _set_urlencoded_form(self, form_data: Sequence[tuple[str, str]]) -> None:
"""
Sets the body to the URL-encoded form data, and adds the appropriate content-type header.
This will overwrite the existing content if there is one.
Expand All @@ -989,23 +990,22 @@ def urlencoded_form(self) -> multidict.MultiDictView[str, str]:
def urlencoded_form(self, value):
self._set_urlencoded_form(value)

def _get_multipart_form(self):
def _get_multipart_form(self) -> list[tuple[bytes, bytes]]:
is_valid_content_type = (
"multipart/form-data" in self.headers.get("content-type", "").lower()
)
if is_valid_content_type and self.content is not None:
try:
return multipart.decode(self.headers.get("content-type"), self.content)
return multipart.decode_multipart(
self.headers.get("content-type"), self.content
)
except ValueError:
pass
return ()
return []

def _set_multipart_form(self, value):
is_valid_content_type = (
self.headers.get("content-type", "")
.lower()
.startswith("multipart/form-data")
)
def _set_multipart_form(self, value: list[tuple[bytes, bytes]]) -> None:
ct = self.headers.get("content-type", "")
is_valid_content_type = ct.lower().startswith("multipart/form-data")
if not is_valid_content_type:
"""
Generate a random boundary here.
Expand All @@ -1014,8 +1014,10 @@ def _set_multipart_form(self, value):
on generating the boundary.
"""
boundary = "-" * 20 + binascii.hexlify(os.urandom(16)).decode()
self.headers["content-type"] = f"multipart/form-data; boundary={boundary}"
self.content = multipart.encode(self.headers, value)
self.headers[
"content-type"
] = ct = f"multipart/form-data; boundary={boundary}"
self.content = multipart.encode_multipart(ct, value)

@property
def multipart_form(self) -> multidict.MultiDictView[bytes, bytes]:
Expand All @@ -1032,7 +1034,7 @@ def multipart_form(self) -> multidict.MultiDictView[bytes, bytes]:
)

@multipart_form.setter
def multipart_form(self, value):
def multipart_form(self, value: list[tuple[bytes, bytes]]) -> None:
self._set_multipart_form(value)


Expand Down
43 changes: 34 additions & 9 deletions mitmproxy/net/http/multipart.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
from __future__ import annotations

import mimetypes
import re
import warnings
from typing import Optional
from urllib.parse import quote

from mitmproxy.net.http import headers


def encode(head, l):
k = head.get("content-type")
if k:
k = headers.parse_content_type(k)
if k is not None:
def encode_multipart(content_type: str, parts: list[tuple[bytes, bytes]]) -> bytes:
if content_type:
ct = headers.parse_content_type(content_type)
if ct is not None:
try:
boundary = k[2]["boundary"].encode("ascii")
boundary = quote(boundary)
raw_boundary = ct[2]["boundary"].encode("ascii")
boundary = quote(raw_boundary)
except (KeyError, UnicodeError):
return b""
hdrs = []
for key, value in l:
for key, value in parts:
file_type = (
mimetypes.guess_type(str(key))[0] or "text/plain; charset=utf-8"
)
Expand All @@ -41,9 +43,12 @@ def encode(head, l):
hdrs.append(b"--%b--\r\n" % boundary.encode("utf-8"))
temp = b"\r\n".join(hdrs)
return temp
return b""


def decode(content_type: Optional[str], content: bytes) -> list[tuple[bytes, bytes]]:
def decode_multipart(
content_type: Optional[str], content: bytes
) -> list[tuple[bytes, bytes]]:
"""
Takes a multipart boundary encoded string and returns list of (key, value) tuples.
"""
Expand All @@ -69,3 +74,23 @@ def decode(content_type: Optional[str], content: bytes) -> list[tuple[bytes, byt
r.append((key, value))
return r
return []


def encode(ct, parts): # pragma: no cover
# 2023-02
warnings.warn(
"multipart.encode is deprecated, use multipart.encode_multipart instead.",
DeprecationWarning,
stacklevel=2,
)
return encode_multipart(ct, parts)


def decode(ct, content): # pragma: no cover
# 2023-02
warnings.warn(
"multipart.decode is deprecated, use multipart.decode_multipart instead.",
DeprecationWarning,
stacklevel=2,
)
return encode_multipart(ct, content)
2 changes: 1 addition & 1 deletion mitmproxy/tools/console/grideditor/editors.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def set_data(self, vals, flow):

class RequestMultipartEditor(base.FocusEditor):
title = "Edit Multipart Form"
columns = [col_text.Column("Key"), col_text.Column("Value")]
columns = [col_bytes.Column("Key"), col_bytes.Column("Value")]

def get_data(self, flow):
return flow.request.multipart_form.items(multi=True)
Expand Down
28 changes: 18 additions & 10 deletions test/mitmproxy/net/http/test_multipart.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import pytest

from mitmproxy.http import Headers
from mitmproxy.net.http import multipart


Expand All @@ -15,23 +14,28 @@ def test_decode():
"value2\n"
"--{0}--".format(boundary).encode()
)
form = multipart.decode(f"multipart/form-data; boundary={boundary}", content)
form = multipart.decode_multipart(
f"multipart/form-data; boundary={boundary}", content
)

assert len(form) == 2
assert form[0] == (b"field1", b"value1")
assert form[1] == (b"field2", b"value2")

boundary = "boundary茅莽"
result = multipart.decode(f"multipart/form-data; boundary={boundary}", content)
result = multipart.decode_multipart(
f"multipart/form-data; boundary={boundary}", content
)
assert result == []

assert multipart.decode("", content) == []
assert multipart.decode_multipart("", content) == []


def test_encode():
data = [(b"file", b"shell.jpg"), (b"file_size", b"1000")]
headers = Headers(content_type="multipart/form-data; boundary=127824672498")
content = multipart.encode(headers, data)
content = multipart.encode_multipart(
"multipart/form-data; boundary=127824672498", data
)

assert b'Content-Disposition: form-data; name="file"' in content
assert (
Expand All @@ -42,9 +46,13 @@ def test_encode():
assert len(content) == 252

with pytest.raises(ValueError, match=r"boundary found in encoded string"):
multipart.encode(headers, [(b"key", b"--127824672498")])
multipart.encode_multipart(
"multipart/form-data; boundary=127824672498", [(b"key", b"--127824672498")]
)

boundary = "boundary茅莽"
headers = Headers(content_type="multipart/form-data; boundary=" + boundary)
result = multipart.encode(headers, data)
result = multipart.encode_multipart(
"multipart/form-data; boundary=boundary茅莽", data
)
assert result == b""

assert multipart.encode_multipart("", data) == b""
2 changes: 1 addition & 1 deletion test/mitmproxy/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ def test_get_multipart_form(self):
request.headers["Content-Type"] = "multipart/form-data"
assert list(request.multipart_form.items()) == []

with mock.patch("mitmproxy.net.http.multipart.decode") as m:
with mock.patch("mitmproxy.net.http.multipart.decode_multipart") as m:
m.side_effect = ValueError
assert list(request.multipart_form.items()) == []

Expand Down

0 comments on commit 7da3a8e

Please sign in to comment.