Repository: matrix-org/synapse

Issue:
`StateFilter.must_await_full_state` falls over if called on a `StateFilter`s for membership events with a bogus state key
```
IndexError: list index out of range
  File "synapse/http/server.py", line 366, in _async_render_wrapper
    callback_return = await self._async_render(request)
  File "synapse/http/server.py", line 572, in _async_render
    callback_return = await raw_callback_return
  File "synapse/rest/client/room.py", line 167, in on_GET
    data = await msg_handler.get_room_data(
  File "synapse/handlers/message.py", line 128, in get_room_data
    data = await self._storage_controllers.state.get_current_state_event(
  File "synapse/storage/controllers/state.py", line 482, in get_current_state_event
    state_map = await self.get_current_state(
  File "synapse/storage/controllers/state.py", line 464, in get_current_state
    state_map_ids = await self.get_current_state_ids(room_id, state_filter)
  File "synapse/storage/controllers/state.py", line 392, in get_current_state_ids
    if not state_filter or state_filter.must_await_full_state(self._is_mine_id):
  File "synapse/storage/state.py", line 563, in must_await_full_state
    if not is_mine_id(state_key):
  File "synapse/server.py", line 341, in is_mine_id
    return string.split(":", 1)[1] == self.hostname
```

Sentry:
https://sentry.tools.element.io/organizations/element/issues/124/events/d3c32827e3264d689cd3985618361192/?project=2&query=firstRelease%3A1.61.0rc1&sort=freq#tags

Matrix.org received a request of the form `GET /_matrix/client/r0/rooms/ROOM_IDstate/m.room.member/foo@example.com`. After checking the requester is in the room, we lookup the given state key in our server's view of the room.:

https://github.com/matrix-org/synapse/blob/a164a46038b0e51142781619db0e6dec8e0c2aaa/synapse/handlers/message.py#L127-L130

This constructs a StateFilter looking for events matching that (type, state_key) pair.

https://github.com/matrix-org/synapse/blob/7c6b2204d143550d81e5bf9612c4e69fe0866b4c/synapse/storage/controllers/state.py#L479-L488

We eventually fall into `must_await_full_state`. Because the event is a membership event, we check to see if the sender is local.

https://github.com/matrix-org/synapse/blob/7c6b2204d143550d81e5bf9612c4e69fe0866b4c/synapse/storage/state.py#L561-L566

But `is_mind_id` throws because the given state_key doesn't contain a colon.

https://github.com/matrix-org/synapse/blob/1e453053cb12ff084fdcdc2f75c08ced274dff21/synapse/server.py#L339-L341

Options that spring to mind:
1. Add a try/except wrapper around `is_mine_id`. Any failure means the fancy faster join stuff is not available.
2. At the rest level, validate that the state_key is a matrix ID when the type is m.room.membership. The [spec](https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidstateeventtypestatekey) would allow us to return 404 Not Found in this case.

Related: #12489. I don't think this is directly related to faster joins; rather it's collateral damage? Might be of interest to @richvdh anyway.




Discussion:
Considering only `is_mine_id` in isolation: I think we should make it return `False` for malformed user/room IDs or raise a `ValueError`.

Resolving patch:
diff --git a/synapse/server.py b/synapse/server.py
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -341,7 +341,17 @@ def is_mine(self, domain_specific_string: DomainSpecificString) -> bool:
         return domain_specific_string.domain == self.hostname
 
     def is_mine_id(self, string: str) -> bool:
-        return string.split(":", 1)[1] == self.hostname
+        """Determines whether a user ID or room alias originates from this homeserver.
+
+        Returns:
+            `True` if the hostname part of the user ID or room alias matches this
+            homeserver.
+            `False` otherwise, or if the user ID or room alias is malformed.
+        """
+        localpart_hostname = string.split(":", 1)
+        if len(localpart_hostname) < 2:
+            return False
+        return localpart_hostname[1] == self.hostname
 
     @cache_in_self
     def get_clock(self) -> Clock:


Test patch:
diff --git a/tests/test_types.py b/tests/test_types.py
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -13,11 +13,35 @@
 # limitations under the License.
 
 from synapse.api.errors import SynapseError
-from synapse.types import RoomAlias, UserID, map_username_to_mxid_localpart
+from synapse.types import (
+    RoomAlias,
+    UserID,
+    get_domain_from_id,
+    get_localpart_from_id,
+    map_username_to_mxid_localpart,
+)
 
 from tests import unittest
 
 
+class IsMineIDTests(unittest.HomeserverTestCase):
+    def test_is_mine_id(self) -> None:
+        self.assertTrue(self.hs.is_mine_id("@user:test"))
+        self.assertTrue(self.hs.is_mine_id("#room:test"))
+        self.assertTrue(self.hs.is_mine_id("invalid:test"))
+
+        self.assertFalse(self.hs.is_mine_id("@user:test\0"))
+        self.assertFalse(self.hs.is_mine_id("@user"))
+
+    def test_two_colons(self) -> None:
+        """Test handling of IDs containing more than one colon."""
+        # The domain starts after the first colon.
+        # These functions must interpret things consistently.
+        self.assertFalse(self.hs.is_mine_id("@user:test:test"))
+        self.assertEqual("user", get_localpart_from_id("@user:test:test"))
+        self.assertEqual("test:test", get_domain_from_id("@user:test:test"))
+
+
 class UserIDTestCase(unittest.HomeserverTestCase):
     def test_parse(self):
         user = UserID.from_string("@1234abcd:test")