Repository: mitmproxy/mitmproxy

Issue:
Doesn't properly check invalid input in tnetstring.parse
#### Problem Description
In the [condiction branch](https://github.com/mitmproxy/mitmproxy/blob/0ca458fd6475ee48728147f3b529467a75e912a4/mitmproxy/io/tnetstring.py#L218) ```data_type == ord(b'}')```of tnetstring.parse(), it doesn't properly check invalid data in the parse loop.

#### Steps to reproduce the behavior:
```
from mitmproxy.io import tnetstring
t = b'51:11:12345678901#4:this,8:true!0:~,4:true!0:]4:\\x00,~}' 
tnetstring.loads(t)

TypeError: unhashable type: 'list'
```
![image](https://user-images.githubusercontent.com/2822242/126905571-521252a6-510a-4e8a-859f-e45fbc59dcbe.png)

#### System Information
Mitmproxy: 7.0.0
Python:    3.8.2
OpenSSL:   OpenSSL 1.1.1k  25 Mar 2021
Platform:  Linux-5.4.0-kali3-amd64-x86_64-with-glibc2.29



Discussion:
I'm not sure if I understand what's the problem here. Is there any way how you can trigger this issue from mitmproxy? Lists aren't supported as dictionary keys in our tnetstring implementation.

 It's somewhat expected that tnetstring.loads borks on invalid input, but I don't understand where that is a Denial-of-Service issue.
> I'm not sure if I understand what's the problem here. Is there any way how you can trigger this issue from mitmproxy? Lists aren't supported as dictionary keys in our tnetstring implementation.
> 
> It's somewhat expected that tnetstring.loads borks on invalid input, but I don't understand where that is a Denial-of-Service issue.

sorry for the description. It just because I found this issue will make a crash.
![image](https://user-images.githubusercontent.com/2822242/127559063-c697b338-dc3f-48d1-9f0e-9da6daa2b619.png)


Resolving patch:
diff --git a/mitmproxy/io/io.py b/mitmproxy/io/io.py
--- a/mitmproxy/io/io.py
+++ b/mitmproxy/io/io.py
@@ -1,5 +1,5 @@
 import os
-from typing import Any, Dict, Iterable, Type, Union, cast  # noqa
+from typing import Any, Dict, IO, Iterable, Type, Union, cast
 
 from mitmproxy import exceptions
 from mitmproxy import flow
@@ -25,8 +25,8 @@ def add(self, flow):
 
 
 class FlowReader:
-    def __init__(self, fo):
-        self.fo = fo
+    def __init__(self, fo: IO[bytes]):
+        self.fo: IO[bytes] = fo
 
     def stream(self) -> Iterable[flow.Flow]:
         """
@@ -46,7 +46,7 @@ def stream(self) -> Iterable[flow.Flow]:
                 if mdata["type"] not in FLOW_TYPES:
                     raise exceptions.FlowReadException("Unknown flow type: {}".format(mdata["type"]))
                 yield FLOW_TYPES[mdata["type"]].from_state(mdata)
-        except ValueError as e:
+        except (ValueError, TypeError) as e:
             if str(e) == "not a tnetstring: empty file":
                 return  # Error is due to EOF
             raise exceptions.FlowReadException("Invalid data format.")
diff --git a/mitmproxy/io/tnetstring.py b/mitmproxy/io/tnetstring.py
--- a/mitmproxy/io/tnetstring.py
+++ b/mitmproxy/io/tnetstring.py
@@ -58,7 +58,7 @@ def dumps(value: TSerializable) -> bytes:
     return b''.join(q)
 
 
-def dump(value: TSerializable, file_handle: typing.BinaryIO) -> None:
+def dump(value: TSerializable, file_handle: typing.IO[bytes]) -> None:
     """
     This function dumps a python object as a tnetstring and
     writes it to the given file.
@@ -156,7 +156,7 @@ def loads(string: bytes) -> TSerializable:
     return pop(string)[0]
 
 
-def load(file_handle: typing.BinaryIO) -> TSerializable:
+def load(file_handle: typing.IO[bytes]) -> TSerializable:
     """load(file) -> object
 
     This function reads a tnetstring from a file and parses it into a


Test patch:
diff --git a/test/mitmproxy/io/test_io.py b/test/mitmproxy/io/test_io.py
--- a/test/mitmproxy/io/test_io.py
+++ b/test/mitmproxy/io/test_io.py
@@ -1 +1,35 @@
-# TODO: write tests
+import io
+
+import pytest
+from hypothesis import example, given
+from hypothesis.strategies import binary
+
+from mitmproxy import exceptions, version
+from mitmproxy.io import FlowReader, tnetstring
+
+
+class TestFlowReader:
+    @given(binary())
+    @example(b'51:11:12345678901#4:this,8:true!0:~,4:true!0:]4:\\x00,~}')
+    def test_fuzz(self, data):
+        f = io.BytesIO(data)
+        reader = FlowReader(f)
+        try:
+            for _ in reader.stream():
+                pass
+        except exceptions.FlowReadException:
+            pass  # should never raise anything else.
+
+    def test_empty(self):
+        assert list(FlowReader(io.BytesIO(b"")).stream()) == []
+
+    def test_unknown_type(self):
+        with pytest.raises(exceptions.FlowReadException, match="Unknown flow type"):
+            weird_flow = tnetstring.dumps({"type": "unknown", "version": version.FLOW_FORMAT_VERSION})
+            for _ in FlowReader(io.BytesIO(weird_flow)).stream():
+                pass
+
+    def test_cannot_migrate(self):
+        with pytest.raises(exceptions.FlowReadException, match="cannot read files with flow format version 0"):
+            for _ in FlowReader(io.BytesIO(b"14:7:version;1:0#}")).stream():
+                pass