From 012fe6edad13d2bf776cf5d28688f7f59951074b Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 26 Sep 2026 14:15:10 +0200 Subject: [PATCH] fix: stop a crafted winmail.dat from freezing the tab A MAPI property in a TNEF attachment declares how many values follow. The parser looped that many times even when a value could not be read and nothing was consumed, so a 31-byte winmail.dat claiming 0xFFFFFFFF values froze the tab for about 13 seconds on every open, longer with more such properties. The count is now clamped to what the block can hold and the loop stops as soon as a value consumes no bytes. --- lib/__tests__/tnef.test.ts | Bin 4311 -> 5135 bytes lib/tnef.ts | 21 ++++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/__tests__/tnef.test.ts b/lib/__tests__/tnef.test.ts index a3e64c453665e7e2acacb7561627bac62260ea0e..67cfc7f7ab5f5c6ad9e5f8daff612407cd2efa0e 100644 GIT binary patch delta 568 zcmZuu&5Bbo6b5%4aCYuL7t(~eljgP{jza}qI20TaEx0IE(%Vz-WzwYF`c6V;h>| z&G_}`<;A0J2EER8PSfmvQ;oZFzI9ksYsSRQiVlFYvjmt*(XA;qwF@v2-W7y}uVJ=$ z5U{HKit4yQVNJEH+y~Zreh4HVf$7A1Yi4A#G(KhtiQyA`Om=!b406mik-}qUCtN;# zHA;hMOCRW#1%+>1gb7z6srTOvw*7-Z#uaOQOY>iZvkhYFZ%GkLwry%c^d6z~b>tgL zZ|9(Ea(hjcfQ5G4>F!4-`#bCX?#c6`Kg@(^*zOR#W8V4F@bVS^me_s2 f+duiYC~=AekSh9T^nyYdm&S=)>(BR&zufr=TpqOt delta 12 TcmeCzxURS%jc@Zj!9pegA+rRj diff --git a/lib/tnef.ts b/lib/tnef.ts index f154048a5..a620fd17a 100644 --- a/lib/tnef.ts +++ b/lib/tnef.ts @@ -165,6 +165,15 @@ function decodeMAPIString(data: Uint8Array, propType: number): string { return new TextDecoder('utf-8').decode(data.subarray(0, len)); } +/** + * Read a multi-value count. Every value takes at least four bytes, so a count + * larger than what is left of the block is a lie; clamp it rather than loop + * up to 2^32 times on the UI thread. + */ +function readValueCount(r: BinaryReader): number { + return Math.min(r.readUint32LE(), Math.floor(r.remaining / 4)); +} + /** Parse MAPI properties from a raw attribute data block */ function parseMAPIProps(data: Uint8Array): Map { const props = new Map(); @@ -200,19 +209,25 @@ function parseMAPIProps(data: Uint8Array): Map 0; j++) { - lastValue = readMAPIVarValue(r); + const value = readMAPIVarValue(r); + // A truncated value consumes nothing; retrying it would spin on the + // same bytes for as many rounds as the sender's count asks for. + if (value === null) break; + lastValue = value; } if (!isMultiValue && lastValue) { props.set(propID, { type: propType, value: lastValue }); } } else if (isMultiValue) { if (r.remaining < 4) break; - const valueCount = r.readUint32LE(); + const valueCount = readValueCount(r); for (let j = 0; j < valueCount && r.remaining > 0; j++) { + const before = r.remaining; readMAPIFixedValue(r, baseType); + if (r.remaining >= before) break; } } else { const value = readMAPIFixedValue(r, baseType);