Skip to content

BerReader.readString returns null on extreme long-form length (0xFFFFFFFF) and accepts non-canonical long-form lengths (DER strictness / split-brain) #27

@franrojasblaze

Description

@franrojasblaze

Summary
I observed inconsistent ASN.1 TLV length handling in asn1-ber BerReader. In several non-canonical DER length encodings, BerReader.readString() accepts the input and returns a Buffer. For an extreme long-form length value (0xFFFFFFFF), readString() returns null (no thrown error). In strict DER contexts, these inputs are typically rejected, and the null soft-fail may be surprising for callers.

This also creates a potential split-brain surface in systems that mix parsers (e.g., asn1-ber in one service and stricter DER parsers elsewhere), where the same bytes are accepted by one component but rejected by another.

Environment
Node.js: (paste your version, e.g. node -v)
OS: Windows (PowerShell)
Package: asn1-ber (paste npm ls asn1-ber)

Reproduction (minimal PoC)
Install:
npm i asn1-ber node-forge
Save as poc_splitbrain_asn1_len.js:
const { BerReader } = require("asn1-ber");
const forge = require("node-forge");

const CASES = [
{ name: "non-canonical long-form len=0 with trailing bytes", hex: "048100222222" },
{ name: "non-canonical long-form len=1 with trailing bytes", hex: "048101222222" },
{ name: "non-canonical long-form len=1 encoded as 00 01", hex: "04820001222222" },
{ name: "extreme long-form len=0xFFFFFFFF", hex: "0484ffffffff222222" },
];

function parseAsn1Ber(buf) {
try {
const r = new BerReader(buf);
const out = r.readString(0x04, true); // 0x04 = OCTET STRING, Buffer output
if (out === null) return "NULL_RETURN";
return ACCEPT(outLen=${out.length});
} catch (e) {
return CRASH(${e.name}: ${e.message});
}
}

function parseForge(buf) {
try {
forge.asn1.fromDer(buf.toString("binary"), true); // strict DER comparison
return "ACCEPT";
} catch (e) {
return CRASH(${e.name}: ${e.message});
}
}

for (const c of CASES) {
const buf = Buffer.from(c.hex, "hex");
console.log("\nCASE:", c.name);
console.log("HEX :", c.hex);
console.log("asn1-ber :", parseAsn1Ber(buf));
console.log("node-forge:", parseForge(buf));
}

Run:

node poc_splitbrain_asn1_len.js

Observed output (from my runs)

048100222222 → asn1-ber: ACCEPT(outLen=0)

048101222222 → asn1-ber: ACCEPT(outLen=1)

04820001222222 → asn1-ber: ACCEPT(outLen=1)

0484ffffffff222222 → asn1-ber: NULL_RETURN (no exception)

For comparison, node-forge (strict DER) rejects these with parse errors such as:

“Unparsed DER bytes remain after ASN.1 parsing.”

“Negative length: -1” (for 0xFFFFFFFF)

Expected / Suggested behavior
In DER-strict contexts, non-minimal length encodings (leading zeros / unnecessary long-form) and trailing bytes should typically be rejected or at least optionally rejected via a “strict” mode.

For the extreme length case (0xFFFFFFFF), returning null is a soft-fail that may surprise callers; it may be safer to throw an explicit error (or clearly document that readString may return null on invalid lengths).

Why this matters
NULL_RETURN can be misinterpreted by consumers as “field absent/optional/empty”, potentially leading to logic bypass if callers don’t treat it as a parse failure.

Mixed-parser systems can become vulnerable to differential parsing (“split-brain”), where one component accepts and another rejects the same message.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions