Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Discard null-containing strings before updating the user directory #12762

Merged
merged 7 commits into from
May 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions changelog.d/12762.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a long-standing bug where the user directory background process would fail to make forward progress if a user included a null codepoint in their display name or avatar.
4 changes: 2 additions & 2 deletions synapse/rest/client/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,10 @@ def __init__(self, hs: "HomeServer"):
self.auth = hs.get_auth()

def register(self, http_server: HttpServer) -> None:
# /room/$roomid/state/$eventtype
# /rooms/$roomid/state/$eventtype
no_state_key = "/rooms/(?P<room_id>[^/]*)/state/(?P<event_type>[^/]*)$"

# /room/$roomid/state/$eventtype/$statekey
# /rooms/$roomid/state/$eventtype/$statekey
state_key = (
"/rooms/(?P<room_id>[^/]*)/state/"
"(?P<event_type>[^/]*)/(?P<state_key>[^/]*)$"
Expand Down
4 changes: 1 addition & 3 deletions synapse/storage/databases/main/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from synapse.types import JsonDict, StateMap, get_domain_from_id
from synapse.util import json_encoder
from synapse.util.iterutils import batch_iter, sorted_topologically
from synapse.util.stringutils import non_null_str_or_none

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand Down Expand Up @@ -1728,9 +1729,6 @@ def _store_room_members_txn(
not affect the current local state.
"""

def non_null_str_or_none(val: Any) -> Optional[str]:
return val if isinstance(val, str) and "\u0000" not in val else None

self.db_pool.simple_insert_many_txn(
txn,
table="room_memberships",
Expand Down
9 changes: 4 additions & 5 deletions synapse/storage/databases/main/user_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from typing_extensions import TypedDict

from synapse.api.errors import StoreError
from synapse.util.stringutils import non_null_str_or_none

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand Down Expand Up @@ -469,11 +470,9 @@ async def update_profile_in_user_dir(
"""
Update or add a user's profile in the user directory.
"""
# If the display name or avatar URL are unexpected types, overwrite them.
if not isinstance(display_name, str):
display_name = None
if not isinstance(avatar_url, str):
avatar_url = None
# If the display name or avatar URL are unexpected types, replace with None.
display_name = non_null_str_or_none(display_name)
avatar_url = non_null_str_or_none(avatar_url)

def _update_profile_in_user_dir_txn(txn: LoggingTransaction) -> None:
self.db_pool.simple_upsert_txn(
Expand Down
10 changes: 9 additions & 1 deletion synapse/util/stringutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import re
import secrets
import string
from typing import Iterable, Optional, Tuple
from typing import Any, Iterable, Optional, Tuple

from netaddr import valid_ipv6

Expand Down Expand Up @@ -247,3 +247,11 @@ def base62_encode(num: int, minwidth: int = 1) -> str:
# pad to minimum width
pad = "0" * (minwidth - len(res))
return pad + res


def non_null_str_or_none(val: Any) -> Optional[str]:
"""Check that the arg is a string containing no null (U+0000) codepoints.

If so, returns the given string unmodified; otherwise, returns None.
"""
return val if isinstance(val, str) and "\u0000" not in val else None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring please :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to a point release for this?

It's not a regression AFAICS, but it has hit a lot of people recently. I'm not sure why we've not seen it on matrix.org? I'll leave the decision to @babolivier as maintainer this week.

Copy link
Contributor

@babolivier babolivier May 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As David says it's not a regression, though it is weird that reports have started flowing in around the time of release (though seems to be purely coincidental). It doesn't seem to affect too many users so I'd say it's probably fine to not do a point release, but I'll keep an eye on it this week and if more people come up reporting this issue I'll cherry-pick this patch into a point release.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After internal discussion, I've realised that the impact is high enough to justify a point release, so let's make it happen.

28 changes: 28 additions & 0 deletions tests/handlers/test_user_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,34 @@ def test_local_user_leaving_room_remains_in_user_directory(self) -> None:
self.assertEqual(in_public, {(bob, room1), (bob, room2)})
self.assertEqual(in_private, set())

def test_ignore_display_names_with_null_codepoints(self) -> None:
MXC_DUMMY = "mxc://dummy"

# Alice creates a public room.
alice = self.register_user("alice", "pass")

# Alice has a user directory entry to start with.
self.assertIn(
alice,
self.get_success(self.user_dir_helper.get_profiles_in_user_directory()),
)

# Alice changes her name to include a null codepoint.
self.get_success(
self.hs.get_user_directory_handler().handle_local_profile_change(
alice,
ProfileInfo(
display_name="abcd\u0000efgh",
avatar_url=MXC_DUMMY,
),
)
)
# Alice's profile should be updated with the new avatar, but no display name.
self.assertEqual(
self.get_success(self.user_dir_helper.get_profiles_in_user_directory()),
{alice: ProfileInfo(display_name=None, avatar_url=MXC_DUMMY)},
)


class TestUserDirSearchDisabled(unittest.HomeserverTestCase):
servlets = [
Expand Down