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

Commit d4713d3

Browse files
author
David Robertson
authored
Discard null-containing strings before updating the user directory (#12762)
1 parent 8afb7b5 commit d4713d3

File tree

6 files changed

+45
-11
lines changed

6 files changed

+45
-11
lines changed

changelog.d/12762.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

synapse/rest/client/room.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@ def __init__(self, hs: "HomeServer"):
109109
self.auth = hs.get_auth()
110110

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

115-
# /room/$roomid/state/$eventtype/$statekey
115+
# /rooms/$roomid/state/$eventtype/$statekey
116116
state_key = (
117117
"/rooms/(?P<room_id>[^/]*)/state/"
118118
"(?P<event_type>[^/]*)/(?P<state_key>[^/]*)$"

synapse/storage/databases/main/events.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from synapse.types import JsonDict, StateMap, get_domain_from_id
5353
from synapse.util import json_encoder
5454
from synapse.util.iterutils import batch_iter, sorted_topologically
55+
from synapse.util.stringutils import non_null_str_or_none
5556

5657
if TYPE_CHECKING:
5758
from synapse.server import HomeServer
@@ -1728,9 +1729,6 @@ def _store_room_members_txn(
17281729
not affect the current local state.
17291730
"""
17301731

1731-
def non_null_str_or_none(val: Any) -> Optional[str]:
1732-
return val if isinstance(val, str) and "\u0000" not in val else None
1733-
17341732
self.db_pool.simple_insert_many_txn(
17351733
txn,
17361734
table="room_memberships",

synapse/storage/databases/main/user_directory.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from typing_extensions import TypedDict
3030

3131
from synapse.api.errors import StoreError
32+
from synapse.util.stringutils import non_null_str_or_none
3233

3334
if TYPE_CHECKING:
3435
from synapse.server import HomeServer
@@ -469,11 +470,9 @@ async def update_profile_in_user_dir(
469470
"""
470471
Update or add a user's profile in the user directory.
471472
"""
472-
# If the display name or avatar URL are unexpected types, overwrite them.
473-
if not isinstance(display_name, str):
474-
display_name = None
475-
if not isinstance(avatar_url, str):
476-
avatar_url = None
473+
# If the display name or avatar URL are unexpected types, replace with None.
474+
display_name = non_null_str_or_none(display_name)
475+
avatar_url = non_null_str_or_none(avatar_url)
477476

478477
def _update_profile_in_user_dir_txn(txn: LoggingTransaction) -> None:
479478
self.db_pool.simple_upsert_txn(

synapse/util/stringutils.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import re
1717
import secrets
1818
import string
19-
from typing import Iterable, Optional, Tuple
19+
from typing import Any, Iterable, Optional, Tuple
2020

2121
from netaddr import valid_ipv6
2222

@@ -247,3 +247,11 @@ def base62_encode(num: int, minwidth: int = 1) -> str:
247247
# pad to minimum width
248248
pad = "0" * (minwidth - len(res))
249249
return pad + res
250+
251+
252+
def non_null_str_or_none(val: Any) -> Optional[str]:
253+
"""Check that the arg is a string containing no null (U+0000) codepoints.
254+
255+
If so, returns the given string unmodified; otherwise, returns None.
256+
"""
257+
return val if isinstance(val, str) and "\u0000" not in val else None

tests/handlers/test_user_directory.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,34 @@ def test_local_user_leaving_room_remains_in_user_directory(self) -> None:
10071007
self.assertEqual(in_public, {(bob, room1), (bob, room2)})
10081008
self.assertEqual(in_private, set())
10091009

1010+
def test_ignore_display_names_with_null_codepoints(self) -> None:
1011+
MXC_DUMMY = "mxc://dummy"
1012+
1013+
# Alice creates a public room.
1014+
alice = self.register_user("alice", "pass")
1015+
1016+
# Alice has a user directory entry to start with.
1017+
self.assertIn(
1018+
alice,
1019+
self.get_success(self.user_dir_helper.get_profiles_in_user_directory()),
1020+
)
1021+
1022+
# Alice changes her name to include a null codepoint.
1023+
self.get_success(
1024+
self.hs.get_user_directory_handler().handle_local_profile_change(
1025+
alice,
1026+
ProfileInfo(
1027+
display_name="abcd\u0000efgh",
1028+
avatar_url=MXC_DUMMY,
1029+
),
1030+
)
1031+
)
1032+
# Alice's profile should be updated with the new avatar, but no display name.
1033+
self.assertEqual(
1034+
self.get_success(self.user_dir_helper.get_profiles_in_user_directory()),
1035+
{alice: ProfileInfo(display_name=None, avatar_url=MXC_DUMMY)},
1036+
)
1037+
10101038

10111039
class TestUserDirSearchDisabled(unittest.HomeserverTestCase):
10121040
servlets = [

0 commit comments

Comments
 (0)