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.
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.