-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Ensure that we correctly auth events returned by send_join
#11012
Changes from all commits
d37c283
44a0762
3701fee
50f58a0
32e9237
8c751f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Fix a long-standing bug which meant that events received over federation were sometimes incorrectly accepted into the room state. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -362,6 +362,7 @@ async def on_send_membership_event( | |
# need to. | ||
await self._event_creation_handler.cache_joined_hosts_for_event(event, context) | ||
|
||
await self._check_for_soft_fail(event, None, origin=origin) | ||
await self._run_push_actions_and_persist_event(event, context) | ||
return event, context | ||
|
||
|
@@ -403,29 +404,28 @@ async def process_remote_join( | |
"""Persists the events returned by a send_join | ||
|
||
Checks the auth chain is valid (and passes auth checks) for the | ||
state and event. Then persists the auth chain and state atomically. | ||
Persists the event separately. Notifies about the persisted events | ||
where appropriate. | ||
|
||
Will attempt to fetch missing auth events. | ||
state and event. Then persists all of the events. | ||
Notifies about the persisted events where appropriate. | ||
|
||
Args: | ||
origin: Where the events came from | ||
room_id, | ||
room_id: | ||
auth_events | ||
state | ||
event | ||
room_version: The room version we expect this room to have, and | ||
will raise if it doesn't match the version in the create event. | ||
|
||
Returns: | ||
The stream ID after which all events have been persisted. | ||
|
||
Raises: | ||
SynapseError if the response is in some way invalid. | ||
""" | ||
events_to_context = {} | ||
for e in itertools.chain(auth_events, state): | ||
e.internal_metadata.outlier = True | ||
events_to_context[e.event_id] = EventContext.for_outlier() | ||
|
||
event_map = { | ||
e.event_id: e for e in itertools.chain(auth_events, state, [event]) | ||
} | ||
event_map = {e.event_id: e for e in itertools.chain(auth_events, state)} | ||
|
||
create_event = None | ||
for e in auth_events: | ||
|
@@ -445,64 +445,36 @@ async def process_remote_join( | |
if room_version.identifier != room_version_id: | ||
raise SynapseError(400, "Room version mismatch") | ||
|
||
missing_auth_events = set() | ||
for e in itertools.chain(auth_events, state, [event]): | ||
for e_id in e.auth_event_ids(): | ||
if e_id not in event_map: | ||
missing_auth_events.add(e_id) | ||
|
||
for e_id in missing_auth_events: | ||
m_ev = await self._federation_client.get_pdu( | ||
[origin], | ||
e_id, | ||
room_version=room_version, | ||
outlier=True, | ||
timeout=10000, | ||
) | ||
if m_ev and m_ev.event_id == e_id: | ||
event_map[e_id] = m_ev | ||
else: | ||
logger.info("Failed to find auth event %r", e_id) | ||
# filter out any events we have already seen | ||
seen_remotes = await self._store.have_seen_events(room_id, event_map.keys()) | ||
for s in seen_remotes: | ||
event_map.pop(s, None) | ||
|
||
for e in itertools.chain(auth_events, state, [event]): | ||
auth_for_e = [ | ||
event_map[e_id] for e_id in e.auth_event_ids() if e_id in event_map | ||
] | ||
if create_event: | ||
auth_for_e.append(create_event) | ||
# persist the auth chain and state events. | ||
# | ||
# any invalid events here will be marked as rejected, and we'll carry on. | ||
# | ||
# any events whose auth events are missing (ie, not in the send_join response, | ||
# and not already in our db) will just be ignored. This is correct behaviour, | ||
# because the reason that auth_events are missing might be due to us being | ||
# unable to validate their signatures. The fact that we can't validate their | ||
# signatures right now doesn't mean that we will *never* be able to, so it | ||
# is premature to reject them. | ||
# | ||
await self._auth_and_persist_outliers(room_id, event_map.values()) | ||
|
||
try: | ||
validate_event_for_room_version(room_version, e) | ||
check_auth_rules_for_event(room_version, e, auth_for_e) | ||
except SynapseError as err: | ||
# we may get SynapseErrors here as well as AuthErrors. For | ||
# instance, there are a couple of (ancient) events in some | ||
# rooms whose senders do not have the correct sigil; these | ||
# cause SynapseErrors in auth.check. We don't want to give up | ||
# the attempt to federate altogether in such cases. | ||
|
||
logger.warning("Rejecting %s because %s", e.event_id, err.msg) | ||
|
||
if e == event: | ||
raise | ||
events_to_context[e.event_id].rejected = RejectedReason.AUTH_ERROR | ||
|
||
if auth_events or state: | ||
await self.persist_events_and_notify( | ||
room_id, | ||
[ | ||
(e, events_to_context[e.event_id]) | ||
for e in itertools.chain(auth_events, state) | ||
], | ||
# and now persist the join event itself. | ||
logger.info("Peristing join-via-remote %s", event) | ||
with nested_logging_context(suffix=event.event_id): | ||
context = await self._state_handler.compute_event_context( | ||
event, old_state=state | ||
) | ||
|
||
new_event_context = await self._state_handler.compute_event_context( | ||
event, old_state=state | ||
) | ||
context = await self._check_event_auth(origin, event, context) | ||
if context.rejected: | ||
raise SynapseError(400, "Join event was rejected") | ||
|
||
return await self.persist_events_and_notify( | ||
room_id, [(event, new_event_context)] | ||
) | ||
return await self.persist_events_and_notify(room_id, [(event, context)]) | ||
|
||
@log_function | ||
async def backfill( | ||
|
@@ -975,9 +947,15 @@ async def _process_received_pdu( | |
) -> None: | ||
"""Called when we have a new non-outlier event. | ||
|
||
This is called when we have a new event to add to the room DAG - either directly | ||
via a /send request, retrieved via get_missing_events after a /send request, or | ||
backfilled after a client request. | ||
This is called when we have a new event to add to the room DAG. This can be | ||
due to: | ||
* events received directly via a /send request | ||
* events retrieved via get_missing_events after a /send request | ||
* events backfilled after a client request. | ||
Comment on lines
+950
to
+954
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this isn't new - just a clarification while I was looking at it. |
||
|
||
It's not currently used for events received from incoming send_{join,knock,leave} | ||
requests (which go via on_send_membership_event), nor for joins created by a | ||
remote join dance (which go via process_remote_join). | ||
|
||
We need to do auth checks and put it through the StateHandler. | ||
|
||
|
@@ -1013,11 +991,19 @@ async def _process_received_pdu( | |
logger.exception("Unexpected AuthError from _check_event_auth") | ||
raise FederationError("ERROR", e.code, e.msg, affected=event.event_id) | ||
|
||
if not backfilled and not context.rejected: | ||
# For new (non-backfilled and non-outlier) events we check if the event | ||
# passes auth based on the current state. If it doesn't then we | ||
# "soft-fail" the event. | ||
await self._check_for_soft_fail(event, state, origin=origin) | ||
|
||
await self._run_push_actions_and_persist_event(event, context, backfilled) | ||
|
||
if backfilled: | ||
if backfilled or context.rejected: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. previously, we were doing the later stuff even if the event was rejected, which I think was a bug. |
||
return | ||
|
||
await self._maybe_kick_guest_users(event) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. previously, we were doing this before persisting the event (which, at best, would create an unnecessary fork in the event dag), and also doing it for |
||
|
||
# For encrypted messages we check that we know about the sending device, | ||
# if we don't then we mark the device cache for that user as stale. | ||
if event.type == EventTypes.Encrypted: | ||
|
@@ -1318,14 +1304,14 @@ def prep(event: EventBase) -> Optional[Tuple[EventBase, EventContext]]: | |
for auth_event_id in event.auth_event_ids(): | ||
ae = persisted_events.get(auth_event_id) | ||
if not ae: | ||
# the fact we can't find the auth event doesn't mean it doesn't | ||
# exist, which means it is premature to reject `event`. Instead we | ||
# just ignore it for now. | ||
logger.warning( | ||
"Event %s relies on auth_event %s, which could not be found.", | ||
"Dropping event %s, which relies on auth_event %s, which could not be found", | ||
event, | ||
auth_event_id, | ||
) | ||
# the fact we can't find the auth event doesn't mean it doesn't | ||
# exist, which means it is premature to reject `event`. Instead we | ||
# just ignore it for now. | ||
return None | ||
auth.append(ae) | ||
|
||
|
@@ -1445,10 +1431,6 @@ async def _check_event_auth( | |
except AuthError as e: | ||
logger.warning("Failed auth resolution for %r because %s", event, e) | ||
context.rejected = RejectedReason.AUTH_ERROR | ||
return context | ||
|
||
await self._check_for_soft_fail(event, state, backfilled, origin=origin) | ||
await self._maybe_kick_guest_users(event) | ||
|
||
return context | ||
|
||
|
@@ -1468,7 +1450,6 @@ async def _check_for_soft_fail( | |
self, | ||
event: EventBase, | ||
state: Optional[Iterable[EventBase]], | ||
backfilled: bool, | ||
origin: str, | ||
) -> None: | ||
"""Checks if we should soft fail the event; if so, marks the event as | ||
|
@@ -1477,15 +1458,8 @@ async def _check_for_soft_fail( | |
Args: | ||
event | ||
state: The state at the event if we don't have all the event's prev events | ||
backfilled: Whether the event is from backfill | ||
origin: The host the event originates from. | ||
""" | ||
# For new (non-backfilled and non-outlier) events we check if the event | ||
# passes auth based on the current state. If it doesn't then we | ||
# "soft-fail" the event. | ||
if backfilled or event.internal_metadata.is_outlier(): | ||
return | ||
Comment on lines
-1483
to
-1487
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the |
||
|
||
extrem_ids_list = await self._store.get_latest_event_ids_in_room(event.room_id) | ||
extrem_ids = set(extrem_ids_list) | ||
prev_event_ids = set(event.prev_event_ids()) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the call to
check_event_auth
is a fair way up, so we're now only doing the soft_fail check if all the other checks pass.