@aunucore/baileys 1.0.74 โ†’ 1.0.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -128,10 +128,9 @@ Everything else is the upstream core. The patch layer stays small and deliberate
128
128
 
129
129
  ### ๐Ÿ“ฐ Recent releases
130
130
 
131
+ - **1.0.76 โ€” Hunt round 3: the decoder + trust-boundary batch.** Every claim in an external deep-audit was verified against the tree (with repros) before anything was patched. The WABinary decoder gets a 512-level nesting cap (a ~100 KB stanza previously recursed 20,000 deep into a raw stack-overflow RangeError) and a 128 MiB zlib output cap (a max-size frame could inflate ~1000ร— toward OOM). Media downloads now verify the WhatsApp HMAC on full downloads โ€” tampered or truncated media errors out instead of decrypting silently โ€” and download hosts are allowlisted to WhatsApp/Facebook CDN domains, closing a sender-driven SSRF (foreign hosts fall back to `mmg.whatsapp.net`). Auth files are written `0600` (they hold private keys; old `0644` files self-heal on next save), GraphQL error bodies are drained instead of pinning keep-alive sockets, `link_code_companion_reg` key material is length-validated before any pairing secret is derived, the session watchdog also hears raw WebSocket frames (buffered-event delays no longer read as a false deaf session), and a dead `taskTimeout` in the mutex factory is removed. Deliberately untouched, with recorded reasoning: `isTrustedIdentity` (upstream TOFU), unbounded host store arrays, and the app-state transaction design.
132
+ - **1.0.75 โ€” Partial-config safety + watchdog cleanup.** A partial `transactionOpts` in the socket config can no longer kill every key-store transaction: the config is deep-merged over the defaults and the transaction layer falls back per-field (`maxCommitRetries` 10, `delayBetweenTriesMs` 3000, retries clamped to at least 1 so an explicit `0` means exactly one attempt) โ€” previously the shallow config spread replaced the whole default object, `maxCommitRetries` came out `undefined`, and every transaction threw `failed to commit transaction mutations` without a single commit attempt. The session watchdog's interval is now stopped when the socket ends (its `stop()` existed but was never called anywhere), closing a one-timer-and-socket-graph leak per reconnect cycle.
131
133
  - **1.0.74 โ€” Fix-all batch.** The entire 1.0.74 heartbeat queue, closed in one version: `resyncAppState` can no longer spin forever holding the key-store transaction mutex (server-omitted collections finish with a warning, plus a 25-round cap), `startByte`-only media downloads no longer silently return an empty stream, an all-failed sticker pack throws Boom 400 instead of uploading empty, key-material guards now check TYPES (Buffer/string only) at six sites so wrong-typed creds fail descriptively instead of as raw TypeErrors, the noise handshake guards a missing `noiseKey` up front, the pre-login `creds.me` dereference family is closed (acks for `<unavailable>` stanzas survive fresh pairing), and two per-entity maps (`RateLimiter.hits`, bug-guard `recentBySender`) evict stale keys with a lazy sweep.
132
- - **1.0.73 โ€” Heartbeat queue + a new README.** Three verified heartbeat fixes: a corrupt frame header can no longer wedge `decodeFrame` (zero-length headers are skipped, frames over 10 MiB fail loudly instead of buffering forever), `botRecentSecretsByChat` is capped at 1,000 chats FIFO, and bare key-store writes are serialized through the transaction mutex so a `migrateSession` commit can't overwrite a fresher session record. Plus a fully rewritten README and 46 curated npm keywords.
133
- - **1.0.72 โ€” Send integrity.** Fixes a critical regression in 1.0.71 that made `sendMessage` relay an empty message, plus auditor-verified fixes: media-cache contamination, sent-message cache LID/PN resolution, newsletter media stanza typing, USync backoff wired with a 60s clamp, and a 30-item heartbeat sweep (HB-61โ€ฆHB-90).
134
-
135
134
  Full changelog for every version since 1.0.2: **[docs](https://docsjs.aunucore.eu.cc)** ยท **[GitHub Releases](https://github.com/AunuCore/AunuBaileys-lib/releases)**.
136
135
 
137
136
  ### ๐Ÿ™ Credits
@@ -68,7 +68,12 @@ export const executeWWWGraphQL = async (docId, variables, accessToken, dataPath
68
68
  body
69
69
  });
70
70
  if (!res.ok) {
71
- throw new Boom(`GraphQL HTTP error ${res.status}`, { statusCode: res.status });
71
+ // AunuBaileys (1.0.76): Hunt-ronde-3 โ€” drain the error body before
72
+ // throwing. An unconsumed fetch body pins its keep-alive socket
73
+ // out of the undici pool until GC; repeated GraphQL errors would
74
+ // steadily starve the pool. The body text also improves the error.
75
+ const errBody = await res.text().catch(() => '');
76
+ throw new Boom(`GraphQL HTTP error ${res.status}${errBody ? `: ${errBody.slice(0, 200)}` : ''}`, { statusCode: res.status });
72
77
  }
73
78
  const json = await res.json();
74
79
  return parseGraphQLResponse(json, dataPath);
@@ -104,7 +109,9 @@ export const executeWamoGraphQL = async (docId, variables, wamoAuth = {}, dataPa
104
109
  body: params.toString()
105
110
  });
106
111
  if (!res.ok) {
107
- throw new Boom(`Wamo GraphQL HTTP error ${res.status}`, { statusCode: res.status });
112
+ // AunuBaileys (1.0.76): drain the error body (see executeWWWGraphQL).
113
+ const errBody = await res.text().catch(() => '');
114
+ throw new Boom(`Wamo GraphQL HTTP error ${res.status}${errBody ? `: ${errBody.slice(0, 200)}` : ''}`, { statusCode: res.status });
108
115
  }
109
116
  const json = await res.json();
110
117
  return parseGraphQLResponse(json, dataPath);
@@ -16,6 +16,15 @@ const makeWASocket = (config) => {
16
16
  ...DEFAULT_CONNECTION_CONFIG,
17
17
  ...config
18
18
  };
19
+ // AunuBaileys (1.0.75, HB-104): transactionOpts is merged per-field โ€”
20
+ // the shallow spread above replaced the whole default object, so a
21
+ // host passing a PARTIAL transactionOpts silently lost the other
22
+ // field's default (and, before the auth-utils fallback, every
23
+ // key-store commit with it).
24
+ newConfig.transactionOpts = {
25
+ ...DEFAULT_CONNECTION_CONFIG.transactionOpts,
26
+ ...(config?.transactionOpts ?? {})
27
+ };
19
28
  // If the user hasn't provided their own history sync function,
20
29
  // let's create a default one that respects the syncFullHistory flag.
21
30
  if (config.shouldSyncHistoryMessage === undefined) {
@@ -30,7 +39,18 @@ const makeWASocket = (config) => {
30
39
  // config.sessionWatchdog (object = options, true = defaults).
31
40
  // It only reports; it never reconnects.
32
41
  if (newConfig.sessionWatchdog) {
33
- sock.aunuSessionWatchdog = attachSessionWatchdog(sock, newConfig.sessionWatchdog === true ? undefined : newConfig.sessionWatchdog);
42
+ const watchdog = attachSessionWatchdog(sock, newConfig.sessionWatchdog === true ? undefined : newConfig.sessionWatchdog);
43
+ sock.aunuSessionWatchdog = watchdog;
44
+ // AunuBaileys (1.0.75, HB-105): stop() existed but was never
45
+ // called anywhere in the lib โ€” the interval is unref'd yet its
46
+ // closure holds ev + sock, so every ended socket with the
47
+ // watchdog enabled leaked one 30s timer and its whole socket
48
+ // graph for the life of the process, one per reconnect cycle.
49
+ // Register stop() as a socket-end handler, the same pattern as
50
+ // stopOfflineDrain (HB-66). stop() is idempotent (safe on the
51
+ // double-end path), and sockets without a watchdog never reach
52
+ // this branch.
53
+ sock.registerSocketEndHandler?.(() => watchdog.stop());
34
54
  }
35
55
  sock.aunuHandler = new AunuHandler(sock);
36
56
  attachAunuAutoJoin(sock, newConfig);
@@ -523,6 +523,19 @@ export const makeMessagesRecvSocket = (config) => {
523
523
  const ref = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_ref'));
524
524
  const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'primary_identity_pub'));
525
525
  const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_wrapped_primary_ephemeral_pub'));
526
+ // AunuBaileys (1.0.76): Hunt-ronde-3 โ€” toRequiredBuffer only
527
+ // checks PRESENCE. The wrapped ephemeral key has a fixed
528
+ // wire shape (32B salt + 16B IV + 32B encrypted pubkey =
529
+ // 80) and an identity pubkey is 32B raw / 33B prefixed;
530
+ // anything else is a malformed stanza. Most bad lengths
531
+ // already died inside libsignal BEFORE advSecretKey was
532
+ // assigned, but 31/32-byte inputs slipped through and
533
+ // derived a garbage advSecretKey that then got persisted.
534
+ // Refuse up front instead of deriving creds from junk.
535
+ if (primaryEphemeralPublicKeyWrapped.length < 80 ||
536
+ (primaryIdentityPublicKey.length !== 32 && primaryIdentityPublicKey.length !== 33)) {
537
+ throw new Boom('link_code_companion_reg: malformed key material in server stanza โ€” refusing to derive pairing secrets from it', { statusCode: 400 });
538
+ }
526
539
  const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped);
527
540
  const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey);
528
541
  const random = randomBytes(32);
@@ -89,8 +89,8 @@ export type SignalKeyStoreWithTransaction = SignalKeyStore & {
89
89
  transaction<T>(exec: () => Promise<T>): Promise<T>;
90
90
  };
91
91
  export type TransactionCapabilityOptions = {
92
- maxCommitRetries: number;
93
- delayBetweenTriesMs: number;
92
+ maxCommitRetries?: number;
93
+ delayBetweenTriesMs?: number;
94
94
  };
95
95
  export type SignalAuthState = {
96
96
  creds: SignalCreds;
@@ -87,7 +87,17 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) {
87
87
  * @param logger logger to log events
88
88
  * @returns SignalKeyStore with transaction capability
89
89
  */
90
- export const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetweenTriesMs }) => {
90
+ export const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetweenTriesMs } = {}) => {
91
+ // AunuBaileys (1.0.75, HB-104): a partial transactionOpts from the
92
+ // host (makeWASocket's shallow spread used to replace the whole
93
+ // default object) left maxCommitRetries undefined, so the commit
94
+ // loop below never ran even once and EVERY mutating transaction
95
+ // threw 'failed to commit transaction mutations' without a single
96
+ // commit attempt. Fall back per-field to the Defaults values, and
97
+ // clamp retries to at least 1 so an explicit 0 means exactly one
98
+ // attempt, matching host intent.
99
+ const commitRetries = Math.max(1, maxCommitRetries ?? 10);
100
+ const commitDelayMs = delayBetweenTriesMs ?? 3000;
91
101
  // number of queries made to the DB during the transaction
92
102
  // only there for logging purposes
93
103
  let dbQueriesInTransaction = 0;
@@ -116,7 +126,7 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
116
126
  logger.trace('committing transaction');
117
127
  // retry mechanism to ensure we've some recovery
118
128
  // in case a transaction fails in the first attempt
119
- let tries = maxCommitRetries;
129
+ let tries = commitRetries;
120
130
  let committed = false;
121
131
  let lastCommitError;
122
132
  while (tries) {
@@ -131,7 +141,7 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
131
141
  catch (error) {
132
142
  lastCommitError = error;
133
143
  logger.warn(`failed to commit ${Object.keys(mutations).length} mutations, tries left=${tries}`);
134
- await delay(delayBetweenTriesMs);
144
+ await delay(commitDelayMs);
135
145
  }
136
146
  }
137
147
  if (!committed) {
@@ -1,7 +1,13 @@
1
1
  export const makeMutex = () => {
2
2
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
3
  let task = Promise.resolve();
4
- let taskTimeout;
4
+ // AunuBaileys (1.0.76): Hunt-ronde-3 โ€” the old `taskTimeout` was dead
5
+ // code (declared, cleared in a finally, NEVER assigned โ€” there is no
6
+ // hidden timeout safety net, and inventing one that breaks the chain
7
+ // would corrupt the serialization this mutex exists for). Removed.
8
+ // Note: a task that never settles blocks later tasks by design โ€” that
9
+ // is what a serial mutex means (upstream parity); callers must not
10
+ // await unbounded work inside mutex sections.
5
11
  return {
6
12
  mutex(code) {
7
13
  task = (async () => {
@@ -11,14 +17,8 @@ export const makeMutex = () => {
11
17
  await task;
12
18
  }
13
19
  catch { }
14
- try {
15
- // execute the current task
16
- const result = await code();
17
- return result;
18
- }
19
- finally {
20
- clearTimeout(taskTimeout);
21
- }
20
+ // execute the current task
21
+ return await code();
22
22
  })();
23
23
  // we replace the existing task, appending the new piece of execution to it
24
24
  // so the next task will have to wait for this one to finish
@@ -601,11 +601,24 @@ const toSmallestChunkSize = (num) => {
601
601
  return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE;
602
602
  };
603
603
  export const getUrlFromDirectPath = (directPath, fallbackHost) => `https://${fallbackHost || DEF_HOST}${directPath}`;
604
+ // AunuBaileys (1.0.76): Hunt-ronde-3 โ€” WhatsApp media is only ever served
605
+ // from WhatsApp/Facebook CDN hosts. A sender-supplied URL whose host is
606
+ // anything else is not a media URL, it is an SSRF attempt (the bot would
607
+ // happily GET an internal https host with the sender's directPath glued
608
+ // on). Non-WA hosts are never fetched: with a directPath we fall back to
609
+ // the canonical media host (the path is globally valid there); without
610
+ // one the download is refused.
611
+ const WA_MEDIA_HOST_SUFFIXES = ['.whatsapp.net', '.fbcdn.net'];
612
+ const isWAMediaHost = (hostname) => !!hostname &&
613
+ (hostname === 'whatsapp.net' || hostname === 'fbcdn.net' ||
614
+ WA_MEDIA_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix)));
604
615
  export const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
605
616
  // AunuBaileys (1.0.69): the sender-supplied URL used to be trusted
606
617
  // verbatim (host + path) โ€” a remote peer could trigger a GET to an
607
618
  // arbitrary https host. Like upstream v7, only the host is taken
608
619
  // from the URL; the path is rebuilt from our own directPath.
620
+ // AunuBaileys (1.0.76): the host is now ALSO allowlisted (above) โ€”
621
+ // a non-WA host falls back to mmg.whatsapp.net instead of being fetched.
609
622
  let downloadUrl;
610
623
  if (directPath) {
611
624
  let host;
@@ -613,10 +626,18 @@ export const downloadContentFromMessage = async ({ mediaKey, directPath, url },
613
626
  host = new URL(url || '').hostname || undefined;
614
627
  }
615
628
  catch { }
629
+ if (!isWAMediaHost(host)) {
630
+ host = undefined;
631
+ }
616
632
  downloadUrl = getUrlFromDirectPath(directPath, host);
617
633
  }
618
634
  else {
619
- downloadUrl = url?.startsWith('https://') ? url : undefined;
635
+ let parsed;
636
+ try {
637
+ parsed = new URL(url || '');
638
+ }
639
+ catch { }
640
+ downloadUrl = parsed?.protocol === 'https:' && isWAMediaHost(parsed.hostname) ? url : undefined;
620
641
  }
621
642
  if (!downloadUrl) {
622
643
  throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 });
@@ -628,7 +649,7 @@ export const downloadContentFromMessage = async ({ mediaKey, directPath, url },
628
649
  * Decrypts and downloads an AES256-CBC encrypted file given the keys.
629
650
  * Assumes the SHA256 of the plaintext is appended to the end of the ciphertext
630
651
  * */
631
- export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
652
+ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv, macKey }, { startByte, endByte, options } = {}) => {
632
653
  let bytesFetched = 0;
633
654
  let startChunk = 0;
634
655
  let firstBlockIsIV = false;
@@ -661,6 +682,35 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
661
682
  });
662
683
  let remainingBytes = Buffer.from([]);
663
684
  let aes;
685
+ // AunuBaileys (1.0.76): Hunt-ronde-3 โ€” the downloaded ciphertext used
686
+ // to be decrypted with NO integrity check: the 10-byte HMAC-SHA256
687
+ // (macKey, iv || ciphertext) that WhatsApp appends to every media
688
+ // file was silently dropped with the trailing partial block, so a
689
+ // tampering server/CDN could alter media undetected (AES-CBC padding
690
+ // only catches damage to the final block). For FULL downloads (no
691
+ // byte range) with a macKey available, verify it: HMAC every
692
+ // ciphertext byte except a held-back final 10, compare in final().
693
+ // Ranged downloads fetch a slice that excludes the MAC โ€” they skip
694
+ // verification, exactly like before. Consumers of the stream still
695
+ // receive plaintext progressively, but a failed check errors the
696
+ // stream in final(), so buffered consumers (downloadMediaMessage
697
+ // 'buffer') discard the content wholesale.
698
+ const verifyMac = !!macKey && !startByte && !endByte;
699
+ const macHmac = verifyMac ? Crypto.createHmac('sha256', macKey).update(iv) : null;
700
+ let macTail = Buffer.alloc(0);
701
+ const feedMac = (chunk) => {
702
+ if (!macHmac) {
703
+ return;
704
+ }
705
+ const joined = macTail.length ? Buffer.concat([macTail, chunk]) : chunk;
706
+ if (joined.length > 10) {
707
+ macHmac.update(joined.subarray(0, joined.length - 10));
708
+ macTail = joined.subarray(joined.length - 10);
709
+ }
710
+ else {
711
+ macTail = joined;
712
+ }
713
+ };
664
714
  const pushBytes = (bytes, push) => {
665
715
  if (startByte || endByte) {
666
716
  const start = bytesFetched >= startByte ? undefined : Math.max(startByte - bytesFetched, 0);
@@ -680,6 +730,7 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
680
730
  };
681
731
  const output = new Transform({
682
732
  transform(chunk, _, callback) {
733
+ feedMac(chunk);
683
734
  let data = Buffer.concat([remainingBytes, chunk]);
684
735
  const decryptLength = toSmallestChunkSize(data.length);
685
736
  remainingBytes = data.slice(decryptLength);
@@ -715,11 +766,19 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
715
766
  }
716
767
  try {
717
768
  pushBytes(aes.final(), b => this.push(b));
718
- callback();
719
769
  }
720
770
  catch (error) {
721
771
  callback(error);
772
+ return;
773
+ }
774
+ if (macHmac) {
775
+ const expectedMac = macHmac.digest().subarray(0, 10);
776
+ if (macTail.length !== 10 || !Crypto.timingSafeEqual(Buffer.from(macTail), expectedMac)) {
777
+ callback(new Boom('Downloaded media failed HMAC integrity verification โ€” the file was tampered with or truncated in transit; refusing it', { statusCode: 400 }));
778
+ return;
779
+ }
722
780
  }
781
+ callback();
723
782
  }
724
783
  });
725
784
  // AunuBaileys (1.0.71): stream.pipe() does NOT forward source
@@ -8,8 +8,10 @@
8
8
  // - 'connection.update' { sessionWatchdog: { quietForMs, lastInboundAt, at } }
9
9
  // once per quiet episode (re-armed by the next inbound message).
10
10
  // Inbound traffic is observed on the event buffer ('messages.upsert' /
11
- // 'messages.update'); hosts that see raw frames can also call
12
- // watchdog.markInbound() directly. Wiring is opt-in: lib/Socket/index.js
11
+ // 'messages.update') AND โ€” since 1.0.76 โ€” on the raw WebSocket frame
12
+ // stream when sock.ws is reachable (buffered delivery is deferred during
13
+ // long syncs, which used to read as a false deaf session); hosts can
14
+ // also call watchdog.markInbound() directly. Wiring is opt-in: lib/Socket/index.js
13
15
  // attaches it only when config.sessionWatchdog is set (object = options,
14
16
  // true = defaults). The interval timer is unref'd so it never keeps a
15
17
  // process alive on its own.
@@ -84,6 +86,12 @@ export const makeSessionWatchdog = (sock, options = {}) => {
84
86
  ev.off?.('messages.upsert', markInbound);
85
87
  ev.off?.('messages.update', markInbound);
86
88
  ev.off?.('connection.update', onConnectionUpdate);
89
+ if (rawWs) {
90
+ rawWs.off?.('message', markInbound);
91
+ if (!rawWs.off && typeof rawWs.removeListener === 'function') {
92
+ rawWs.removeListener('message', markInbound);
93
+ }
94
+ }
87
95
  return watchdog;
88
96
  };
89
97
  const getState = () => ({
@@ -95,6 +103,19 @@ export const makeSessionWatchdog = (sock, options = {}) => {
95
103
  ev.on('messages.upsert', markInbound);
96
104
  ev.on('messages.update', markInbound);
97
105
  ev.on('connection.update', onConnectionUpdate);
106
+ // AunuBaileys (1.0.76): Hunt-ronde-3 โ€” ALSO observe the raw WebSocket
107
+ // frame stream when available. messages.upsert/update travel through
108
+ // the event buffer, which DEFERS delivery to listeners while a
109
+ // buffered function runs (app-state resync, history processing): real
110
+ // inbound messages then fail to re-arm the watchdog for the whole
111
+ // buffering window and a long sync reads as a deaf session โ€” a false
112
+ // alert. Raw frames are never buffered, and for a deaf-session
113
+ // detector any inbound frame (message, receipt, IQ, keep-alive reply)
114
+ // is the truer liveness signal anyway. Detached again in stop().
115
+ const rawWs = sock.ws && typeof sock.ws.on === 'function' ? sock.ws : null;
116
+ if (rawWs) {
117
+ rawWs.on('message', markInbound);
118
+ }
98
119
  const watchdog = {
99
120
  markInbound,
100
121
  noteInbound: markInbound,
@@ -45,7 +45,14 @@ export const useMultiFileAuthState = async (folder, logger) => {
45
45
  try {
46
46
  // AunuBaileys: atomic write (tmp + rename) anti-korupsi sesi โ€” terinspirasi @itsliaaa/baileys
47
47
  const tmpPath = `${filePath}.tmp-${process.pid}`;
48
- await writeFile(tmpPath, JSON.stringify(data, BufferJSON.replacer));
48
+ // AunuBaileys (1.0.76): Hunt-ronde-3 โ€” these files hold
49
+ // private keys (creds.json, session/sender-key records).
50
+ // They used to be created with default permissions (0644
51
+ // on a typical umask) โ€” readable by every local user on a
52
+ // shared host. Write the tmp file 0600; the rename carries
53
+ // the mode onto the target, so pre-existing 0644 files
54
+ // also self-heal to 0600 on their next save.
55
+ await writeFile(tmpPath, JSON.stringify(data, BufferJSON.replacer), { mode: 0o600 });
49
56
  await rename(tmpPath, filePath);
50
57
  }
51
58
  catch (err) {
@@ -3,9 +3,24 @@ import { inflate } from 'zlib';
3
3
  import * as constants from './constants.js';
4
4
  import { jidEncode } from './jid-utils.js';
5
5
  const inflatePromise = promisify(inflate);
6
+ // AunuBaileys (1.0.76): Hunt-ronde-3 โ€” inflate used to run with NO output
7
+ // cap: zlib expands up to ~1000x, so a max-size wire frame (10 MiB, HB-91)
8
+ // could inflate to ~10 GB and OOM the process. 128 MiB is far above any
9
+ // legitimate stanza (wire stanzas are messages / IQ results / app-state
10
+ // patches โ€” big payloads travel as media downloads, not stanzas) while
11
+ // still stopping a bomb cold. Exceeding it is a clean decode failure.
12
+ const MAX_DECOMPRESSED_SIZE = 128 * 1024 * 1024;
6
13
  export const decompressingIfRequired = async (buffer) => {
7
14
  if (2 & buffer.readUInt8()) {
8
- buffer = await inflatePromise(buffer.slice(1));
15
+ try {
16
+ buffer = await inflatePromise(buffer.slice(1), { maxOutputLength: MAX_DECOMPRESSED_SIZE });
17
+ }
18
+ catch (err) {
19
+ if (err?.code === 'ERR_BUFFER_TOO_LARGE') {
20
+ throw new Error(`AunuBaileys: compressed stanza exceeds the ${MAX_DECOMPRESSED_SIZE / (1024 * 1024)} MiB decompressed-size cap โ€” refusing to inflate (possible zlib bomb)`);
21
+ }
22
+ throw err;
23
+ }
9
24
  }
10
25
  else {
11
26
  // nodes with no compression have a 0x00 prefix, we remove that
@@ -13,7 +28,18 @@ export const decompressingIfRequired = async (buffer) => {
13
28
  }
14
29
  return buffer;
15
30
  };
16
- export const decodeDecompressedBinaryNode = (buffer, opts, indexRef = { index: 0 }) => {
31
+ // AunuBaileys (1.0.76): Hunt-ronde-3 โ€” the decoder recursed with NO depth
32
+ // bound: ~5 wire bytes buy one nesting level, so a ~100 KB stanza reached
33
+ // depth 20,000 and died as an uncontrolled RangeError (stack overflow).
34
+ // Legit stanzas nest < ~20 deep; 512 is generous headroom, and crossing it
35
+ // is a descriptive decode error instead of a V8 crash. (Total node count
36
+ // needs no separate cap: every node consumes >= 1 input byte, so count is
37
+ // bounded by the โ€” now also capped โ€” buffer length.)
38
+ const MAX_NODE_DEPTH = 512;
39
+ export const decodeDecompressedBinaryNode = (buffer, opts, indexRef = { index: 0 }, depth = 0) => {
40
+ if (depth > MAX_NODE_DEPTH) {
41
+ throw new Error(`AunuBaileys: binary node nesting exceeds the ${MAX_NODE_DEPTH}-level safety cap โ€” refusing to decode (possible depth bomb)`);
42
+ }
17
43
  const { DOUBLE_BYTE_TOKENS, SINGLE_BYTE_TOKENS, TAGS } = opts;
18
44
  const checkEOS = (length) => {
19
45
  if (indexRef.index + length > buffer.length) {
@@ -186,7 +212,7 @@ export const decodeDecompressedBinaryNode = (buffer, opts, indexRef = { index: 0
186
212
  const items = [];
187
213
  const size = readListSize(tag);
188
214
  for (let i = 0; i < size; i++) {
189
- items.push(decodeDecompressedBinaryNode(buffer, opts, indexRef));
215
+ items.push(decodeDecompressedBinaryNode(buffer, opts, indexRef, depth + 1));
190
216
  }
191
217
  return items;
192
218
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aunucore/baileys",
3
3
  "type": "module",
4
- "version": "1.0.74",
4
+ "version": "1.0.76",
5
5
  "description": "AunuBaileys โ€” the self-healing WhatsApp Web API for Node.js. Bad MAC and \"2000 messages into the future\" errors repair themselves, LID and phone-number identities stay in sync, anti-ban is built in, pairing codes are custom โ€” plus 29 experimental features no other Baileys fork ships: invisible watermarks, per-person tag-all cards, side-profile text, ghost mentions and more. ESM, CJS and full TypeScript types.",
6
6
  "keywords": [
7
7
  "whatsapp",