diff --git a/packages/server/src/routes/federation.ts b/packages/server/src/routes/federation.ts index f4e4fb0d..9a4fbf90 100644 --- a/packages/server/src/routes/federation.ts +++ b/packages/server/src/routes/federation.ts @@ -1640,6 +1640,12 @@ function processMemberAddEvent( return; } + // Validate authority: only the owner's instance can add members + if (channel.ownerHomeInstance && sourceInstance !== channel.ownerHomeInstance) { + rejected.push({ messageId: event.messageId, reason: 'unauthorized_source' }); + return; + } + // Cancel soft-delete if channel was pending GC if (channel.deletedAt) { db.update(schema.dmChannels) @@ -1713,6 +1719,12 @@ function processMemberRemoveEvent( return; } + // Validate authority: owner's instance for kicks, any instance for self-leave + if (event.membership.reason !== 'leave' && channel.ownerHomeInstance && sourceInstance !== channel.ownerHomeInstance) { + rejected.push({ messageId: event.messageId, reason: 'unauthorized_source' }); + return; + } + const localUser = resolveLocalUser(event.membership.user.homeUserId, db); if (!localUser) { accepted.push(event.messageId); @@ -1782,6 +1794,12 @@ function processOwnershipTransferEvent( return; } + // Validate authority: only the current owner's instance can transfer ownership + if (channel.ownerHomeInstance && sourceInstance !== channel.ownerHomeInstance) { + rejected.push({ messageId: event.messageId, reason: 'unauthorized_source' }); + return; + } + // Resolve new owner to local user (for local ownerId) const newOwnerLocal = resolveLocalUser(event.ownership.newOwner.homeUserId, db);