Skip to content

Bug report: TLV Parser produces incorrect BER-encoded lengths #2245

@williballenthin

Description

@williballenthin

Describe the bug
The TLVParser library mishandles BER (Basic Encoding Rules) long-form lengths in two ways: it fails to advance past the initial length byte, and it reads multi-byte lengths in little-endian order instead of big-endian.

src/core/lib/TLVParser.mjs, getLength() method, lines 37-52

getLength() {
    if (this.basicEncodingRules) {
        const bit = this.input[this.location];
        if (bit & 0x80) {
            this.bytesInLength = bit & ~0x80;
        } else {
            this.location++;
            return bit & ~0x80;
        }
    }

    let length = 0;

    for (let i = 0; i < this.bytesInLength; i++) {
        length += this.input[this.location] * Math.pow(Math.pow(2, 8), i);
        this.location++;
    }

    return length;
}

In BER, when the first length byte has bit 7 set (long form), the low 7 bits encode the number of subsequent bytes that hold the actual length, in big-endian order. Two bugs:

  1. Missing location advance. In the short-form branch (else), this.location++ skips the length byte before returning. In the long-form branch (if), this.location is never advanced, so the for loop re-reads the initial indicator byte as part of the length value, corrupting the result and misaligning all subsequent TLV parsing.

  2. Wrong byte order. The for loop accumulates bytes with Math.pow(256, i), weighting byte 0 (the first byte read) as the least significant. BER specifies big-endian: the first byte after the indicator is the most significant.

A secondary issue: the code mutates this.bytesInLength, a persistent instance field, which corrupts length parsing for all subsequent TLV entries on the same parser instance.

To Reproduce
use Parse TLV with BER encoding on any input containing a long-form length (e.g., a length of 256, encoded as 82 01 00). The parser will misread the length and produce garbled output.

For example: https://gchq.github.io/CyberChef/#recipe=From_Hex('Auto')Parse_TLV(1,1,true)&input=MDEgODIgMDAgMDUgNDggNjUgNmMgNmMgNmYgMDIgMDMgNDEgNDIgNDM

Image

Additional context

BER documentation:

Image

https://www.oss.com/asn1/resources/asn1-made-simple/asn1-quick-reference/basic-encoding-rules.html

Suggested fix:

getLength() {
    let bytesInLength = this.bytesInLength;
    let bigEndian = false;

    if (this.basicEncodingRules) {
        const firstLengthByte = this.input[this.location];
        this.location++;

        if (firstLengthByte & 0x80) {
            bytesInLength = firstLengthByte & ~0x80;
            bigEndian = true;
        } else {
            return firstLengthByte & ~0x80;
        }
    }

    let length = 0;

    for (let i = 0; i < bytesInLength; i++) {
        if (bigEndian) {
            length = (length << 8) + this.input[this.location];
        } else {
            length += this.input[this.location] * Math.pow(Math.pow(2, 8), i);
        }
        this.location++;
    }

    return length;
}

This always advances past the initial byte, uses big-endian accumulation for BER, preserves little-endian for non-BER callers, and uses a local variable to avoid mutating instance state.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions