Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions src/Socket/messages-recv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
NACK_REASONS,
NO_MESSAGE_FOUND_ERROR_TEXT,
SERVER_ERROR_CODES,
setBotMessageSecret,
toNumber,
unixTimestampSeconds,
xmppPreKey,
Expand Down Expand Up @@ -1583,14 +1584,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}

const handleMessage = async (node: BinaryNode) => {
const encNode = getBinaryNodeChild(node, 'enc')
// TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
if (encNode?.attrs.type === 'msmsg') {
logger.debug({ key: node.attrs.key }, 'ignored msmsg')
await sendMessageAck(node, NACK_REASONS.MissingMessageSecret)
return
}

let acked = false

try {
Expand Down
160 changes: 156 additions & 4 deletions src/Utils/decode-wa-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,37 @@ import {
} from '../WABinary'
import { unpadRandomMax16 } from './generics'
import type { ILogger } from './logger'
import { decodeDecryptedMsmsgMessage, decodeRichResponseMessage, decryptMsmsgBotMessage, type MsmsgMessageKey } from './meta-ai-msmsg'

const MAX_SECRETS_PER_CHAT = 20

const botMessageSecrets = new Map<string, Buffer>()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Unbounded global message-secret cache risks memory growth and cross-session contamination

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Utils/decode-wa-message.ts, line 25:

<comment>Unbounded global message-secret cache risks memory growth and cross-session contamination</comment>

<file context>
@@ -18,6 +18,37 @@ import {
+
+const MAX_SECRETS_PER_CHAT = 20
+
+const botMessageSecrets = new Map<string, Buffer>()
+const botRecentSecretsByChat = new Map<string, { id: string; secret: Buffer }[]>()
+
</file context>

const botRecentSecretsByChat = new Map<string, { id: string; secret: Buffer }[]>()

const pushRecentChatSecret = (chatJid: string, id: string, secretBuf: Buffer): void => {
if (!chatJid || !secretBuf) return
const existing = botRecentSecretsByChat.get(chatJid) || []
const filtered = existing.filter(item => item.id !== id && !item.secret.equals(secretBuf))
filtered.unshift({ id, secret: secretBuf })
if (filtered.length > MAX_SECRETS_PER_CHAT) filtered.length = MAX_SECRETS_PER_CHAT
botRecentSecretsByChat.set(chatJid, filtered)
}

export const setBotMessageSecret = (id: string, secret: Uint8Array | Buffer | string, chatJid?: string): void => {
if (!id || !secret) return
let buf: Buffer
if (Buffer.isBuffer(secret)) {
buf = secret
} else if (secret instanceof Uint8Array) {
buf = Buffer.from(secret.buffer, secret.byteOffset, secret.byteLength)
} else if (typeof secret === 'string') {
buf = Buffer.from(secret, 'base64')
} else {
return
}
botMessageSecrets.set(id, buf)
if (chatJid) pushRecentChatSecret(chatJid, id, buf)
}
Comment on lines +25 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bound botMessageSecrets to prevent unbounded growth

botMessageSecrets never evicts entries. In long-lived sessions this can grow indefinitely and degrade memory usage over time.

Suggested change
 const MAX_SECRETS_PER_CHAT = 20
+const MAX_SECRETS_GLOBAL = 2000
 
 const botMessageSecrets = new Map<string, Buffer>()
+const botMessageSecretOrder: string[] = []
 const botRecentSecretsByChat = new Map<string, { id: string; secret: Buffer }[]>()
@@
 	botMessageSecrets.set(id, buf)
+	botMessageSecretOrder.push(id)
+	if (botMessageSecretOrder.length > MAX_SECRETS_GLOBAL) {
+		const evictId = botMessageSecretOrder.shift()
+		if (evictId) botMessageSecrets.delete(evictId)
+	}
 	if (chatJid) pushRecentChatSecret(chatJid, id, buf)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Utils/decode-wa-message.ts` around lines 25 - 51, botMessageSecrets
currently never evicts entries leading to unbounded memory growth; modify
setBotMessageSecret to enforce a fixed-cap eviction policy (e.g.,
MAX_SECRETS_GLOBAL constant) and remove oldest entries when the map exceeds that
cap. Locate botMessageSecrets and the setBotMessageSecret function and after
inserting the new buf, check botMessageSecrets.size and delete the oldest key(s)
(use botMessageSecrets.keys().next().value to find the oldest) until size <=
MAX_SECRETS_GLOBAL; keep pushRecentChatSecret and botRecentSecretsByChat logic
unchanged.


export const getDecryptionJid = async (sender: string, repository: SignalRepositoryWithLIDStore): Promise<string> => {
if (isLidUser(sender) || isHostedLidUser(sender)) {
Expand Down Expand Up @@ -269,13 +300,30 @@ export const decryptMessageNode = (
logger: ILogger
) => {
const { fullMessage, author, sender } = decodeMessageNode(stanza, meId, meLid)

let metaTargetId: string | null = null
let botEditTargetId: string | null = null
let botType: string | null = null
let metaTargetSenderJid: string | null = null

return {
fullMessage,
category: stanza.attrs.category,
author,
async decrypt() {
let decryptables = 0
if (Array.isArray(stanza.content)) {
// Pre-scan for msmsg metadata nodes
const hasMsmsg = stanza.content.some(({ attrs }) => attrs?.type === 'msmsg')
if (hasMsmsg) {
for (const { tag, attrs } of stanza.content) {
if (tag === 'meta' && attrs?.target_id) metaTargetId = attrs.target_id
if (tag === 'meta' && attrs?.target_sender_jid) metaTargetSenderJid = attrs.target_sender_jid
if (tag === 'bot' && attrs && 'edit_target_id' in attrs) botEditTargetId = attrs.edit_target_id
if (tag === 'bot' && attrs?.edit) botType = attrs.edit
}
}

for (const { tag, attrs, content } of stanza.content) {
if (tag === 'verified_name' && content instanceof Uint8Array) {
const cert = proto.VerifiedNameCertificate.decode(content)
Expand All @@ -301,7 +349,7 @@ export const decryptMessageNode = (

decryptables += 1

let msgBuffer: Uint8Array
let msgBuffer: Uint8Array | Buffer | undefined

const decryptionJid = await getDecryptionJid(author, repository)

Expand Down Expand Up @@ -329,17 +377,100 @@ export const decryptMessageNode = (
ciphertext: content
})
break
case 'msmsg': {
// 'first' = streaming partial — intentionally skip
if (botType !== null && !['full', 'last'].includes(botType)) break

const secretIdCandidates = [botEditTargetId, metaTargetId, fullMessage.key?.id].filter(Boolean) as string[]
const secretCandidates: { source: string; secret: Buffer }[] = []
const seenSecrets = new Set<string>()

for (const idCandidate of secretIdCandidates) {
const byId = botMessageSecrets.get(idCandidate)
if (!byId) continue
const fp = byId.toString('hex')
if (!seenSecrets.has(fp)) {
seenSecrets.add(fp)
secretCandidates.push({ source: `id:${idCandidate}`, secret: byId })
}
}

const chatRecent = botRecentSecretsByChat.get(sender) || []
for (const item of chatRecent) {
const fp = item.secret.toString('hex')
if (!seenSecrets.has(fp)) {
seenSecrets.add(fp)
secretCandidates.push({ source: `chat:${item.id}`, secret: item.secret })
}
if (secretCandidates.length >= 6) break
}

if (!secretCandidates.length) {
logger.warn({ metaTargetId, botType, secretIdCandidates }, 'msmsg: no candidate messageSecret found, skipping')
break
}

const msMsg = proto.MessageSecretMessage.decode(content)
const helperKey: MsmsgMessageKey = {
participant: author,
meId: metaTargetSenderJid || `${meLid.split(':')[0]}@lid`,
meLid,
conversationJid: sender,
senderJid: metaTargetSenderJid || undefined,
botType,
botEditTargetId,
metaTargetId,
stanzaId: stanza.attrs?.id,
targetId: botEditTargetId || metaTargetId || stanza.attrs?.id,
targetIdCandidates: secretIdCandidates
}

let decryptErr: unknown
const candidateAttemptSummaries: object[] = []

for (const candidate of secretCandidates) {
try {
msgBuffer = await decryptMsmsgBotMessage(candidate.secret, helperKey, msMsg)
logger.debug({ source: candidate.source }, 'msmsg: decrypted with candidate secret')
break
} catch (e: any) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify explicit any usage in this file
rg -nP "\bas\s+any\b|:\s*any\b" src/Utils/decode-wa-message.ts

Repository: WhiskeySockets/Baileys

Length of output: 365


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '400,560p' src/Utils/decode-wa-message.ts | nl -ba | sed -n '1,200p'

Repository: WhiskeySockets/Baileys

Length of output: 109


🏁 Script executed:

#!/bin/bash
set -euo pipefail

awk 'NR>=420 && NR<=540 {printf "%d:%s\n", NR, $0}' src/Utils/decode-wa-message.ts
echo "----"
awk 'NR>=540 && NR<=575 {printf "%d:%s\n", NR, $0}' src/Utils/decode-wa-message.ts

Repository: WhiskeySockets/Baileys

Length of output: 5196


Remove new any usage in msmsg/rich-response decryption paths

  • Replace catch (e: any) (line 436) with catch (e: unknown) and narrow before reading attemptedStrategies / rethrowing.
  • Replace as any richResponse augmentation (lines 493 and 498) with a local structural type (e.g., { text?: string }) and cast/narrow from unknown instead of weakening types.
  • If this block was added in the same change, also avoid catch (err: any) (line 512) / isSessionRecordError(error: any) (line 542) by switching to unknown + narrowing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Utils/decode-wa-message.ts` at line 436, The catch blocks in
decode-wa-message decryption paths use `any` and unchecked casts which weaken
type-safety: change `catch (e: any)` to `catch (e: unknown)` and narrow `e`
before accessing properties like `attemptedStrategies` (use type guards or
instanceof checks and rethrow if unknown), replace `as any` enrichments for
`richResponse` with a local structural type (e.g., `{ text?: string }`) and
cast/narrow the unknown value into that shape before mutating, and likewise
convert other `catch (err: any)` and the `isSessionRecordError(error: any)`
usage to accept `unknown` and perform explicit narrowing inside
`isSessionRecordError` (or add a type guard function) so no `any` is introduced
while preserving the same behavior in the decryption and augmentation code
paths.

decryptErr = e
if (Array.isArray(e?.attemptedStrategies) && e.attemptedStrategies.length) {
candidateAttemptSummaries.push({ secretSource: candidate.source, attemptedStrategies: e.attemptedStrategies })
}
}
}

if (!msgBuffer && candidateAttemptSummaries.length) {
logger.warn(
{ secretCandidateSources: secretCandidates.map(c => c.source), attemptsBySecret: candidateAttemptSummaries },
'msmsg: helper decryption failed for all candidate secrets'
)
}
if (!msgBuffer && decryptErr) throw decryptErr
break
}
case 'plaintext':
msgBuffer = content
break
default:
throw new Error(`Unknown e2e type: ${e2eType}`)
}

let msg: proto.IMessage = proto.Message.decode(
e2eType !== 'plaintext' ? unpadRandomMax16(msgBuffer) : msgBuffer
)
if (!msgBuffer) continue

let msg: proto.IMessage =
e2eType === 'msmsg'
? decodeDecryptedMsmsgMessage(msgBuffer)
: proto.Message.decode(e2eType !== 'plaintext' ? unpadRandomMax16(msgBuffer) : msgBuffer)

const outerMessageContextInfo = msg.messageContextInfo
msg = msg.deviceSentMessage?.message || msg
// deviceSentMessage.message may not carry messageContextInfo — preserve it
if (outerMessageContextInfo && !msg.messageContextInfo) {
msg.messageContextInfo = outerMessageContextInfo
}

if (msg.senderKeyDistributionMessage) {
//eslint-disable-next-line max-depth
try {
Expand All @@ -357,6 +488,27 @@ export const decryptMessageNode = (
} else {
fullMessage.message = msg
}

// Auto-decode richResponseMessage text (stored as dynamic property — not in proto schema)
const rich = fullMessage.message?.richResponseMessage as any
if (rich && !rich.text) {
const decoded = decodeRichResponseMessage(rich)
if (decoded) rich.text = decoded
}
const editedRich = fullMessage.message?.protocolMessage?.editedMessage?.richResponseMessage as any
if (editedRich && !editedRich.text) {
const decoded = decodeRichResponseMessage(editedRich)
if (decoded) editedRich.text = decoded
}

// Cache messageSecret for future msmsg decryption
const secret = msg.messageContextInfo?.messageSecret
if (secret) {
const secretBuf = Buffer.isBuffer(secret)
? secret
: Buffer.from((secret as Uint8Array).buffer, (secret as Uint8Array).byteOffset, (secret as Uint8Array).byteLength)
setBotMessageSecret(fullMessage.key.id!, secretBuf, fullMessage.key.remoteJid!)
}
} catch (err: any) {
const errorContext = {
key: fullMessage.key,
Expand Down
Loading
Loading