diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 4e798d12..d499a88c 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -1265,14 +1265,23 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { ) } + // CRITICAL: Normalize JIDs BEFORE acquiring mutex to ensure messages from the same + // chat (arriving with different JID formats - LID vs PN) use the SAME mutex key. + // This prevents parallel processing of messages from the same conversation which + // would break message ordering guarantees. + // Addresses Copilot/Codex PR #75 critical review: JID normalization vulnerability + await normalizeMessageJids(msg, signalRepository, logger) + try { - // Use KeyedMutex with remoteJid to allow parallel processing of messages from different chats - // while maintaining order for messages within the same chat - // Fallback: msg.key.id (unique per message) > 'unknown' (serializes all unknown messages) - const mutexKey = msg.key.remoteJid || (() => { - logger.warn({ msgId: msg.key.id, fromMe: msg.key.fromMe }, 'Missing remoteJid in message key, using msg.key.id as fallback') - return msg.key.id || 'unknown' - })() + // Use KeyedMutex with NORMALIZED remoteJid for parallel processing across different chats + // while maintaining sequential order within the same chat + // Fallback chain: remoteJid (normalized) > msg.key.id (unique) > 'unknown' (serializes all) + let mutexKey = msg.key.remoteJid + if (!mutexKey) { + logger.warn({ msgId: msg.key.id, fromMe: msg.key.fromMe }, 'Missing remoteJid after normalization, using msg.key.id as fallback') + mutexKey = msg.key.id || 'unknown' + } + await messageMutex.mutex(mutexKey, async () => { await decrypt() // message failed to decrypt @@ -1487,7 +1496,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } } - await normalizeMessageJids(msg, signalRepository, logger) + // JID normalization moved BEFORE mutex acquisition (line 1273) to prevent race conditions + // cleanMessage still runs inside mutex to ensure atomic message processing cleanMessage(msg, authState.creds.me!.id, authState.creds.me!.lid!) await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify') diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index a767ec3c..fd8e3c59 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -1639,10 +1639,11 @@ export const makeMessagesSocket = (config: SocketConfig) => { // Emit own event for album root if configured if (config.emitOwnEvents) { process.nextTick(async () => { - const mutexKey = albumRootMsg.key.remoteJid || (() => { + let mutexKey = albumRootMsg.key.remoteJid + if (!mutexKey) { logger.warn({ msgId: albumRootMsg.key.id }, 'Missing remoteJid in albumRootMsg, using msg.key.id as fallback') - return albumRootMsg.key.id || 'unknown' - })() + mutexKey = albumRootMsg.key.id || 'unknown' + } await messageMutex.mutex(mutexKey, () => upsertMessage(albumRootMsg, 'append')) }) } @@ -1736,10 +1737,11 @@ export const makeMessagesSocket = (config: SocketConfig) => { // Emit own event if configured if (config.emitOwnEvents) { process.nextTick(async () => { - const mutexKey = mediaMsg.key.remoteJid || (() => { + let mutexKey = mediaMsg.key.remoteJid + if (!mutexKey) { logger.warn({ msgId: mediaMsg.key.id }, 'Missing remoteJid in mediaMsg, using msg.key.id as fallback') - return mediaMsg.key.id || 'unknown' - })() + mutexKey = mediaMsg.key.id || 'unknown' + } await messageMutex.mutex(mutexKey, () => upsertMessage(mediaMsg, 'append')) }) } @@ -1939,10 +1941,11 @@ export const makeMessagesSocket = (config: SocketConfig) => { }) if (config.emitOwnEvents) { process.nextTick(async () => { - const mutexKey = fullMsg.key.remoteJid || (() => { + let mutexKey = fullMsg.key.remoteJid + if (!mutexKey) { logger.warn({ msgId: fullMsg.key.id }, 'Missing remoteJid in fullMsg, using msg.key.id as fallback') - return fullMsg.key.id || 'unknown' - })() + mutexKey = fullMsg.key.id || 'unknown' + } await messageMutex.mutex(mutexKey, () => upsertMessage(fullMsg, 'append')) }) }