Skip to content

Commit 931ed89

Browse files
guusdkFishbowler
authored andcommitted
OF-2163: Prevent unexpected stanza modifications to leak information
Delivering a stanza to a MUC room occupant changes the addressing of the stanza (the room JID is replaced by the real JID). It is often undesirable that this change is applied to the original stanza (that can be processed further), as such changes can leak the privacy-sensitive real address of the recipient. This commit creates defensive copies of the stanza that is being sent, to prevent leaking the real address of the recipient.
1 parent 625cf58 commit 931ed89

File tree

5 files changed

+70
-36
lines changed

5 files changed

+70
-36
lines changed

xmppserver/src/main/java/org/jivesoftware/openfire/muc/HistoryRequest.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ public void sendHistory(MUCRole joinRole, MUCRoomHistory roomHistory) {
134134
if (!isConfigured()) {
135135
Iterator<Message> history = roomHistory.getMessageHistory();
136136
while (history.hasNext()) {
137-
joinRole.send(history.next());
137+
// OF-2163: Create a defensive copy of the message, to prevent the address that it is sent to to leak back into the archive.
138+
joinRole.send(history.next().createCopy());
138139
}
139140
}
140141
else {
@@ -194,8 +195,9 @@ public void sendHistory(MUCRole joinRole, MUCRoomHistory roomHistory) {
194195
historyToSend.addFirst(message);
195196
}
196197
// Send the smallest amount of traffic to the user
197-
for (Object aHistoryToSend : historyToSend) {
198-
joinRole.send((Message) aHistoryToSend);
198+
for (final Message aHistoryToSend : historyToSend) {
199+
// OF-2163: Create a defensive copy of the message, to prevent the address that it is sent to to leak back into the archive.
200+
joinRole.send(aHistoryToSend.createCopy());
199201
}
200202
}
201203
}

xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCRole.java

+5
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,12 @@ default boolean isRemoteFmuc() {
209209
/**
210210
* Sends a packet to the user.
211211
*
212+
* Note that sending a packet can modify it (notably, the 'to' address can be changed. If this is undesired (for
213+
* example, because post-processing should not expose the modified 'to' address), then a copy of the original
214+
* stanza should be provided as an argument to this method.
215+
*
212216
* @param packet The packet to send
217+
* @see <a href="https://igniterealtime.atlassian.net/browse/OF-2163">issue OF-2163</a>
213218
*/
214219
void send( Packet packet );
215220

xmppserver/src/main/java/org/jivesoftware/openfire/muc/spi/IQMUCvCardHandler.java

+2
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ private void sendVCardUpdateNotification( final MUCRoom room, String hash )
229229

230230
for ( final MUCRole occupant : room.getOccupants() )
231231
{
232+
// Not needed to create a defensive copy of the stanza. It's not used anywhere else.
232233
occupant.send(notification);
233234
}
234235
}
@@ -245,6 +246,7 @@ private void sendConfigChangeNotification( final MUCRoom room )
245246

246247
for ( final MUCRole occupant : room.getOccupants() )
247248
{
249+
// Not needed to create a defensive copy of the stanza. It's not used anywhere else.
248250
occupant.send(notification);
249251
}
250252
}

xmppserver/src/main/java/org/jivesoftware/openfire/muc/spi/LocalMUCRoom.java

+56-32
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,8 @@ public MUCRole joinRoom( @Nonnull String nickname,
698698
final Presence presenceItemNotFound = new Presence(Presence.Type.error);
699699
presenceItemNotFound.setError(PacketError.Condition.item_not_found);
700700
presenceItemNotFound.setFrom(role.getRoleAddress());
701+
702+
// Not needed to create a defensive copy of the stanza. It's not used anywhere else.
701703
joinRole.send(presenceItemNotFound);
702704
}
703705

@@ -722,7 +724,10 @@ private void sendRoomHistoryAfterJoin( @Nonnull final LocalMUCUser user, @Nonnul
722724
Log.trace( "Sending default room history to user '{}' that joined room '{}'.", user.getAddress(), this.getJID() );
723725
final Iterator<Message> history = roomHistory.getMessageHistory();
724726
while (history.hasNext()) {
725-
joinRole.send(history.next());
727+
// OF-2163: Prevent modifying the original history stanza (that can be retrieved by others later) by making a defensive copy.
728+
// This prevents the stanzas in the room history to have a 'to' address for the last user that it was sent to.
729+
final Message message = history.next().createCopy();
730+
joinRole.send(message);
726731
}
727732
} else {
728733
Log.trace( "Sending user-requested room history to user '{}' that joined room '{}'.", user.getAddress(), this.getJID() );
@@ -736,7 +741,10 @@ private void sendRoomHistoryAfterJoin( @Nonnull final LocalMUCUser user, @Nonnul
736741
private void sendRoomSubjectAfterJoin( @Nonnull final LocalMUCUser user, @Nonnull MUCRole joinRole )
737742
{
738743
Log.trace( "Sending room subject to user '{}' that joined room '{}'.", user.getAddress(), this.getJID() );
739-
Message roomSubject = roomHistory.getChangedSubject();
744+
745+
// OF-2163: Prevent modifying the original subject stanza (that can be retrieved by others later) by making a defensive copy.
746+
// This prevents the stanza kept in memory to have the 'to' address for the last user that it was sent to.
747+
Message roomSubject = roomHistory.getChangedSubject().createCopy();
740748

741749
// 7.2.15 If there is no subject set, the room MUST return an empty <subject/> element.
742750
if (roomSubject == null) {
@@ -1034,14 +1042,11 @@ void sendInitialPresencesToNewOccupant(MUCRole joinRole) {
10341042
}
10351043
}
10361044

1037-
final Presence occupantPresence;
1045+
final Presence occupantPresence = occupant.getPresence().createCopy(); // defensive copy to prevent modifying the original.
10381046
if (!canAnyoneDiscoverJID() && MUCRole.Role.moderator != joinRole.getRole()) {
10391047
// Don't include the occupant's JID if the room is semi-anon and the new occupant is not a moderator
1040-
occupantPresence = occupant.getPresence().createCopy(); // defensive copy to prevent modifying the original.
10411048
final Element frag = occupantPresence.getChildElement("x", "http://jabber.org/protocol/muc#user");
10421049
frag.element("item").addAttribute("jid", null);
1043-
} else {
1044-
occupantPresence = occupant.getPresence();
10451050
}
10461051
joinRole.send(occupantPresence);
10471052
}
@@ -1086,8 +1091,8 @@ CompletableFuture<Void> sendLeavePresenceToExistingOccupants(MUCRole leaveRole)
10861091
// Send the presence of this new occupant to existing occupants
10871092
Log.trace( "Send presence of leaving occupant '{}' to existing occupants of room '{}'.", leaveRole.getUserAddress(), leaveRole.getChatRoom().getJID() );
10881093
try {
1089-
Presence originalPresence = leaveRole.getPresence();
1090-
Presence presence = originalPresence.createCopy();
1094+
final Presence originalPresence = leaveRole.getPresence();
1095+
final Presence presence = originalPresence.createCopy(); // Defensive copy to not modify the original.
10911096
presence.setType(Presence.Type.unavailable);
10921097
presence.setStatus(null);
10931098
// Change (or add) presence information about roles and affiliations
@@ -1326,13 +1331,12 @@ public void destroyRoom(DestroyRoomRequest destroyRequest) {
13261331
for (MUCRole removedRole : removedRoles) {
13271332
try {
13281333
// Send a presence stanza of type "unavailable" to the occupant
1329-
Presence presence = createPresence(Presence.Type.unavailable);
1334+
final Presence presence = createPresence(Presence.Type.unavailable);
13301335
presence.setFrom(removedRole.getRoleAddress());
13311336

13321337
// A fragment containing the x-extension for room destruction.
1333-
Element fragment = presence.addChildElement("x",
1334-
"http://jabber.org/protocol/muc#user");
1335-
Element item = fragment.addElement("item");
1338+
final Element fragment = presence.addChildElement("x","http://jabber.org/protocol/muc#user");
1339+
final Element item = fragment.addElement("item");
13361340
item.addAttribute("affiliation", "none");
13371341
item.addAttribute("role", "none");
13381342
if (alternateJID != null) {
@@ -1345,6 +1349,8 @@ public void destroyRoom(DestroyRoomRequest destroyRequest) {
13451349
}
13461350
destroy.addElement("reason").setText(reason);
13471351
}
1352+
1353+
// Not needed to create a defensive copy of the stanza. It's not used anywhere else.
13481354
removedRole.send(presence);
13491355
}
13501356
catch (Exception e) {
@@ -1418,16 +1424,24 @@ public void sendPrivatePacket(Packet packet, MUCRole senderRole) throws NotFound
14181424
if (occupants == null || occupants.size() == 0) {
14191425
throw new NotFoundException();
14201426
}
1421-
if (canAnyoneDiscoverJID && packet instanceof Message) {
1422-
addRealJidToMessage((Message)packet, senderRole);
1427+
1428+
// OF-2163: Prevent modifying the original stanza (that can be used by unrelated code, after this method returns) by making a defensive copy.
1429+
final Packet stanza = packet.createCopy();
1430+
if (canAnyoneDiscoverJID && stanza instanceof Message) {
1431+
addRealJidToMessage((Message)stanza, senderRole);
14231432
}
1424-
for (MUCRole occupant : occupants) {
1425-
packet.setFrom(senderRole.getRoleAddress());
1426-
occupant.send(packet);
1427-
if(packet instanceof Message) {
1428-
Message message = (Message) packet;
1429-
MUCEventDispatcher.privateMessageRecieved(occupant.getUserAddress(), senderRole.getUserAddress(),
1430-
message);
1433+
stanza.setFrom(senderRole.getRoleAddress());
1434+
1435+
// Sending the stanza will modify it. Make sure that the event listeners that are triggered after sending
1436+
// the stanza don't get the 'real' address from the recipient.
1437+
final Packet immutable = stanza.createCopy();
1438+
1439+
// Forward it to each occupant.
1440+
for (final MUCRole occupant : occupants) {
1441+
occupant.send(stanza); // Use the stanza copy to send data. The 'to' address of this object will be changed by sending it.
1442+
if (stanza instanceof Message) {
1443+
// Use an unmodified copy of the stanza (with the original 'to' address) when invoking event listeners (OF-2163)
1444+
MUCEventDispatcher.privateMessageRecieved(occupant.getUserAddress(), senderRole.getUserAddress(), (Message) immutable);
14311445
}
14321446
}
14331447
}
@@ -1491,28 +1505,31 @@ private CompletableFuture<Void> broadcastPresence( Presence presence, boolean is
14911505
throw new IllegalArgumentException("Broadcasted presence stanza's 'from' JID " + presence.getFrom() + " does not match room JID: " + this.getJID());
14921506
}
14931507

1508+
// Create a defensive copy, to prevent modifications to leak back to the invoker.
1509+
final Presence stanza = presence.createCopy();
1510+
14941511
// Some clients send a presence update to the room, rather than to their own nickname.
1495-
if ( JiveGlobals.getBooleanProperty("xmpp.muc.presence.overwrite-to-room", true) && presence.getTo() != null && presence.getTo().getResource() == null && sender.getRoleAddress() != null) {
1496-
presence.setTo( sender.getRoleAddress() );
1512+
if ( JiveGlobals.getBooleanProperty("xmpp.muc.presence.overwrite-to-room", true) && stanza.getTo() != null && stanza.getTo().getResource() == null && sender.getRoleAddress() != null) {
1513+
stanza.setTo( sender.getRoleAddress() );
14971514
}
14981515

1499-
if (!shouldBroadcastPresence(presence)) {
1516+
if (!shouldBroadcastPresence(stanza)) {
15001517
// Just send the presence to the sender of the presence
1501-
sender.send(presence);
1518+
sender.send(stanza);
15021519
return CompletableFuture.completedFuture(null);
15031520
}
15041521

15051522
// If FMUC is active, propagate the presence through FMUC first. Note that when a master-slave mode is active,
15061523
// we need to wait for an echo back, before the message can be broadcasted locally. The 'propagate' method will
15071524
// return a CompletableFuture object that is completed as soon as processing can continue.
1508-
return fmucHandler.propagate(presence, sender)
1525+
return fmucHandler.propagate(stanza, sender)
15091526
.thenRunAsync(() -> {
15101527
// Broadcast presence to occupants hosted by other cluster nodes
1511-
BroadcastPresenceRequest request = new BroadcastPresenceRequest(this, sender, presence, isJoinPresence);
1528+
BroadcastPresenceRequest request = new BroadcastPresenceRequest(this, sender, stanza, isJoinPresence);
15121529
CacheFactory.doClusterTask(request);
15131530

15141531
// Broadcast presence to occupants connected to this JVM
1515-
request = new BroadcastPresenceRequest(this, sender, presence, isJoinPresence);
1532+
request = new BroadcastPresenceRequest(this, sender, stanza, isJoinPresence);
15161533
request.setOriginator(true);
15171534
request.run();
15181535
}
@@ -1687,14 +1704,18 @@ public void broadcast(@Nonnull final BroadcastMessageRequest messageRequest)
16871704
// Add message to the room history
16881705
roomHistory.addMessage(message);
16891706
// Send message to occupants connected to this JVM
1690-
for (MUCRole occupant : occupantsByFullJID.values()) {
1707+
1708+
// Create a defensive copy of the message that will be broadcast, as the broadcast will modify it ('to' addresses
1709+
// will be changed), and it's undesirable to see these modifications in post-processing (OF-2163).
1710+
final Message mutatingCopy = message.createCopy();
1711+
for (final MUCRole occupant : occupantsByFullJID.values()) {
16911712
try
16921713
{
16931714
// Do not send broadcast messages to deaf occupants or occupants hosted in
16941715
// other cluster nodes or other FMUC nodes.
16951716
if ( occupant.isLocal() && !occupant.isVoiceOnly() && !occupant.isRemoteFmuc() )
16961717
{
1697-
occupant.send( message );
1718+
occupant.send( mutatingCopy );
16981719
}
16991720
}
17001721
catch ( Exception e )
@@ -2837,8 +2858,11 @@ private void kickPresence(Presence kickPresence, JID actorJID, String nick) {
28372858
actor.addAttribute("nick", nick);
28382859
}
28392860
}
2840-
// Send the unavailable presence to the banned user
2841-
kickedRole.send(kickPresence);
2861+
2862+
// Send a defensive copy (to not leak a change to the 'to' address - this is possibly overprotective here,
2863+
// but we're erring on the side of caution) of the unavailable presence to the banned user.
2864+
kickedRole.send(kickPresence.createCopy());
2865+
28422866
// Remove the occupant from the room's occupants lists
28432867
OccupantLeftEvent event = new OccupantLeftEvent(this, kickedRole);
28442868
event.setOriginator(true);

xmppserver/src/main/java/org/jivesoftware/openfire/muc/spi/MultiUserChatServiceImpl.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,8 @@ private void broadcastShutdown()
589589
item.addAttribute( "role", "none" );
590590
fragment.addElement( "status" ).addAttribute( "code", "332" );
591591

592-
// Make sure that the presence change for each user is only sent to that user (and not broadcasted in the room)!
592+
// Make sure that the presence change for each user is only sent to that user (and not broadcast in the room)!
593+
// Not needed to create a defensive copy of the stanza. It's not used anywhere else.
593594
role.send( presence );
594595
}
595596
}

0 commit comments

Comments
 (0)