1
0
mirror of synced 2026-05-22 14:43:19 +00:00
Files
music-metadata/test/test-file-asf.ts
T
Borewit 892986136d Harden ASF parser against infinite loop and malformed headers
- Prevent parser rewind by disallowing negative skip lengths in tokenizer
- Validate ASF object sizes to ensure minimum header length
- Add sanity limit (10k) for numberOfHeaderObjects to avoid excessive iteration
2026-04-01 21:22:56 +02:00

186 lines
6.5 KiB
TypeScript

import { assert, expect } from 'chai';
import * as mm from '../lib/index.js';
import path from 'node:path';
import AsfGuid from '../lib/asf/AsfGuid.js';
import { getParserForAttr } from '../lib/asf/AsfUtil.js';
import { AsfContentParseError, DataType } from '../lib/asf/AsfObject.js';
import { Parsers } from './metadata-parsers.js';
import { samplePath } from './util.js';
import type { IPicture } from '../lib/index.js';
const asfFilePath = path.join(samplePath, 'asf');
describe('Parse ASF', () => {
describe('GUID', () => {
it('should construct GUID from string', () => {
const Header_GUID = Uint8Array.from([
0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11,
0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C
]);
assert.deepEqual(AsfGuid.HeaderObject.toBin(), Header_GUID);
});
it('should construct GUID from string', () => {
const guid_data = new Uint8Array([48, 38, 178, 117, 142, 102, 207, 17, 166, 217, 0, 170, 0, 98, 206, 108]);
assert.deepEqual(AsfGuid.fromBin(guid_data).str, '75B22630-668E-11CF-A6D9-00AA0062CE6C');
});
});
/**
* Trying Buffer.readUIntLE(0, 8)
* Where 8 is 2 bytes longer then maximum allowed of 6
*/
it('should be able to roughly decode a 64-bit QWord', () => {
const tests: { raw: number[], expected: number, description: string }[] = [
{
raw: [0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
expected: 0xFF,
description: '8-bit'
},
{
raw: [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
expected: 0xFFFF,
description: '16-bit'
},
{
raw: [0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00],
expected: 0xFFFFFFFF,
description: '32-bit'
},
{
raw: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00],
expected: 0xFFFFFFFFFF,
description: '40-bit'
},
{
raw: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00],
expected: 0xFFFFFFFFFFFF,
description: '48-bit'
},
{
raw: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00],
expected: 0xFFFFFFFFFFFFF,
description: '52-bit'
}
];
tests.forEach(test => {
const buf = Uint8Array.from(test.raw);
assert.strictEqual(Number(getParserForAttr(DataType.QWord)(buf)), test.expected, test.description);
});
});
describe('parse', () => {
function checkFormat(format) {
assert.strictEqual(format.container, 'ASF/audio', 'format.container');
assert.strictEqual(format.codec, 'Windows Media Audio 9.1', 'format.codec');
assert.approximately(format.duration, 243.306, 1 / 10000, 'format.duration');
assert.strictEqual(format.bitrate, 192639, 'format.bitrate');
assert.isTrue(format.hasAudio, 'format.hasAudio');
assert.isFalse(format.hasVideo, 'format.hasVideo');
}
function checkCommon(common) {
assert.strictEqual(common.title, 'Don\'t Bring Me Down', 'common.title');
assert.deepEqual(common.artist, 'Electric Light Orchestra', 'common.artist');
assert.deepEqual(common.albumartist, 'Electric Light Orchestra', 'common.albumartist');
assert.strictEqual(common.album, 'Discovery', 'common.album');
assert.strictEqual(common.year, 2001, 'common.year');
assert.deepEqual(common.track, {no: 9, of: null}, 'common.track 9/0');
assert.deepEqual(common.disk, {no: null, of: null}, 'common.disk 0/0');
assert.deepEqual(common.genre, ['Rock'], 'common.genre');
}
function checkNative(native: mm.INativeTagDict) {
assert.deepEqual(native['WM/AlbumTitle'], ['Discovery'], 'native: WM/AlbumTitle');
assert.deepEqual(native['WM/BeatsPerMinute'], [117], 'native: WM/BeatsPerMinute');
assert.deepEqual(native.REPLAYGAIN_TRACK_GAIN, ['-4.7 dB'], 'native: REPLAYGAIN_TRACK_GAIN');
}
describe('should decode an ASF audio file (.wma)', () => {
Parsers.forEach(parser => {
it(parser.description, async function(){
const {format, common, native} = await parser.parse(() => this.skip(), path.join(asfFilePath, 'asf.wma'), 'audio/x-ms-wma');
checkFormat(format);
checkCommon(common);
assert.isDefined(native, 'metadata.native');
assert.isDefined(native.asf, 'should include native ASF tags');
checkNative(mm.orderTags(native.asf));
});
});
});
describe('should decode picture from', () => {
Parsers.forEach(parser => {
it(parser.description, async function(){
const filePath = path.join(asfFilePath, 'issue_57.wma');
const {native} = await parser.parse(() => this.skip(), filePath, 'audio/x-ms-wma');
const asf = mm.orderTags(native.asf);
assert.exists(asf['WM/Picture'][0], 'ASF WM/Picture should be set');
const nativePicture = asf['WM/Picture'][0];
assert.exists((nativePicture as IPicture).data);
});
});
});
/**
* Related issue: https://github.com/Borewit/music-metadata/issues/68
*/
it('should be able to parse truncated .wma file', async () => {
const filePath = path.join(asfFilePath, '13 Thirty Dirty Birds.wma');
const {format, common, native} = await mm.parseFile(filePath);
assert.strictEqual(format.container, 'ASF/audio', 'format.container');
assert.strictEqual(format.codec, 'Windows Media Audio 9', 'format.codec');
assert.approximately(format.duration, 14.466, 1 / 10000, 'format.duration');
assert.approximately(format.bitrate, 128639, 1, 'format.bitrate');
assert.isTrue(format.hasAudio, 'format.hasAudio');
assert.isFalse(format.hasVideo, 'format.hasVideo'); });
});
describe('security hardening', () => {
it('Avoid infinite loop CWE-835', async () => {
const filePath = path.join(asfFilePath, 'CWE-835.wma');
try {
await mm.parseFile(filePath);
expect.fail('Expected parseFile to throw AsfContentParseError');
} catch (err) {
expect(err).to.be.instanceOf(AsfContentParseError);
expect((err as Error).message).to.match(/Invalid ASF header object size/);
}
});
it('numberOfObjectHeaders=4294967295', async () => {
const filePath = path.join(asfFilePath, 'max-numberOfObjectHeaders.wma');
try {
await mm.parseFile(filePath);
expect.fail('Expected parseFile to throw AsfContentParseError');
} catch (err) {
expect(err).to.be.instanceOf(AsfContentParseError);
expect((err as Error).message).to.match(/Unrealistic number of ASF header objects/);
}
});
});
});