fix: stop a crafted winmail.dat from freezing the app

This commit is contained in:
Linus Rath
2026-10-29 03:05:04 +01:00
parent 942f45179c
commit 855d304b4f
2 changed files with 45 additions and 3 deletions
+27
View File
@@ -65,6 +65,33 @@ describe('parseTnef', () => {
expect(res.htmlBody).toBe('<p>hi</p>');
expect(res.attachments[0]).toMatchObject({ name: 'report.pdf', mimeType: 'application/pdf' });
});
// A sender-chosen value count of 2^32 - 1 over a block with no room for a
// single value used to spin the JS thread on reads that never advance.
it('does not spin on a value count the block cannot hold', () => {
const mapiVar = [
...u32(1),
0x02, 0x01, 0x01, 0x00, // PT_BINARY, id 0x0001
...u32(0xffffffff),
0x00, 0x00,
];
const mapiFixed = [
...u32(1),
0x99, 0x10, 0x01, 0x00, // multi-value of an unknown fixed type
...u32(0xffffffff),
0x00, 0x00,
];
for (const mapi of [mapiVar, mapiFixed]) {
const data = new Uint8Array([
...u32(0x223e9f78), 0x00, 0x00,
...attr(0x01, 0x0002800c, bytes('Hello body')),
...attr(0x01, 0x00069003, mapi),
]);
const started = Date.now();
const res = parseTnef(data);
expect(Date.now() - started).toBeLessThan(1000);
expect(res.body).toBe('Hello body');
}
});
});
describe('isTnefAttachment', () => {
+18 -3
View File
@@ -165,6 +165,15 @@ function decodeMAPIString(data: Uint8Array, propType: number): string {
return decodeUtf8(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 JS thread.
*/
function readValueCount(r: BinaryReader): number {
return Math.min(r.readUint32LE(), Math.floor(r.remaining / 4));
}
function parseMAPIProps(data: Uint8Array): Map<number, { type: number; value: Uint8Array | number | null }> {
const props = new Map<number, { type: number; value: Uint8Array | number | null }>();
const r = new BinaryReader(data);
@@ -198,19 +207,25 @@ function parseMAPIProps(data: Uint8Array): Map<number, { type: number; value: Ui
if (isVarLengthType(baseType)) {
if (r.remaining < 4) break;
const valueCount = r.readUint32LE();
const valueCount = readValueCount(r);
let lastValue: Uint8Array | null = null;
for (let j = 0; j < valueCount && r.remaining > 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);