Affected component: ASN1.parseDER() (and any wrapper that does not enforce full consumption)
Summary:
ASN1.parseDER() successfully parses an ASN.1 element from a DER buffer even when extra unparsed bytes remain in the input. It also accepts non-canonical DER length encodings (long-form lengths for values < 128 and long-form with leading zero). Applications that assume parseDER() performs strict DER validation may accept malformed inputs that strict DER parsers reject, enabling differential parsing and trailing-byte smuggling.
Impact:
If a caller does not enforce full-buffer consumption (i.e., does not check asn1.DER.length === input.length), an attacker can append arbitrary trailing bytes to a DER-encoded element and still have it accepted/parsed. In multi-component pipelines (validation vs verification, different languages/libs, caching, normalization), this can cause interpretation conflicts (“split-brain”), smuggling of hidden data, and canonicalization hazards.
Reproduction (minimal PoC):
Input (hex): 048101222222 (OCTET STRING, non-canonical long-form length=1, with trailing bytes)
Observed with @fidm/asn1: parses successfully, consumed=4, remaining=2
Observed with strict DER parser (node-forge strict): rejects with Unparsed DER bytes remain after ASN.1 parsing.
Code:
const { ASN1, Class, Tag } = require("@fidm/asn1");
const forge = require("node-forge");
function run(hex) {
const raw = Buffer.from(hex, "hex");
// @fidm/asn1: parses prefix only
const el = ASN1.parseDER(raw, Class.UNIVERSAL, Tag.OCTETSTRING);
const consumed = el.DER.length;
const remaining = raw.length - consumed;
console.log("\nHEX:", hex);
console.log("@fidm/asn1 -> parsed OCTET STRING");
console.log(" valueLen:", el.value.length);
console.log(" consumed:", consumed, "/", raw.length, "remaining:", remaining);
// node-forge strict: rejects trailing bytes
try {
const bytes = raw.toString("binary");
forge.asn1.fromDer(bytes, { strict: true });
console.log("forge strict -> ACCEPT (unexpected)");
} catch (e) {
console.log("forge strict -> REJECT:", String(e.message || e));
}
// This is what a vulnerable caller looks like:
// It trusts 'el.value' and ignores trailing.
if (remaining > 0) {
console.log("!!! If caller does NOT enforce full consumption, trailing bytes are silently ignored.");
}
}
// Non-canonical + trailing (your cases)
run("048100222222"); // len=0 (long-form) + 3 trailing bytes
run("048101222222"); // len=1 (long-form) + 2 trailing bytes
run("04820001222222"); // len=1 (leading zero) + 2 trailing bytes
Workaround / Mitigation:
After parsing, reject unless asn1.DER.length === input.length. Additionally, reject non-canonical length encodings (no long-form for lengths < 128; no leading zeros in long-form lengths).
Suggested fix (library):
Provide a strict mode helper that enforces canonical DER (including minimal length encoding) and exact consumption, or make parseDER optionally strict about trailing bytes / canonical length.
Affected component: ASN1.parseDER() (and any wrapper that does not enforce full consumption)
Summary:
ASN1.parseDER() successfully parses an ASN.1 element from a DER buffer even when extra unparsed bytes remain in the input. It also accepts non-canonical DER length encodings (long-form lengths for values < 128 and long-form with leading zero). Applications that assume parseDER() performs strict DER validation may accept malformed inputs that strict DER parsers reject, enabling differential parsing and trailing-byte smuggling.
Impact:
If a caller does not enforce full-buffer consumption (i.e., does not check asn1.DER.length === input.length), an attacker can append arbitrary trailing bytes to a DER-encoded element and still have it accepted/parsed. In multi-component pipelines (validation vs verification, different languages/libs, caching, normalization), this can cause interpretation conflicts (“split-brain”), smuggling of hidden data, and canonicalization hazards.
Reproduction (minimal PoC):
Input (hex): 048101222222 (OCTET STRING, non-canonical long-form length=1, with trailing bytes)
Observed with @fidm/asn1: parses successfully, consumed=4, remaining=2
Observed with strict DER parser (node-forge strict): rejects with Unparsed DER bytes remain after ASN.1 parsing.
Code:
const { ASN1, Class, Tag } = require("@fidm/asn1");
const forge = require("node-forge");
function run(hex) {
const raw = Buffer.from(hex, "hex");
// @fidm/asn1: parses prefix only
const el = ASN1.parseDER(raw, Class.UNIVERSAL, Tag.OCTETSTRING);
const consumed = el.DER.length;
const remaining = raw.length - consumed;
console.log("\nHEX:", hex);
console.log("@fidm/asn1 -> parsed OCTET STRING");
console.log(" valueLen:", el.value.length);
console.log(" consumed:", consumed, "/", raw.length, "remaining:", remaining);
// node-forge strict: rejects trailing bytes
try {
const bytes = raw.toString("binary");
forge.asn1.fromDer(bytes, { strict: true });
console.log("forge strict -> ACCEPT (unexpected)");
} catch (e) {
console.log("forge strict -> REJECT:", String(e.message || e));
}
// This is what a vulnerable caller looks like:
// It trusts 'el.value' and ignores trailing.
if (remaining > 0) {
console.log("!!! If caller does NOT enforce full consumption, trailing bytes are silently ignored.");
}
}
// Non-canonical + trailing (your cases)
run("048100222222"); // len=0 (long-form) + 3 trailing bytes
run("048101222222"); // len=1 (long-form) + 2 trailing bytes
run("04820001222222"); // len=1 (leading zero) + 2 trailing bytes
Workaround / Mitigation:
After parsing, reject unless asn1.DER.length === input.length. Additionally, reject non-canonical length encodings (no long-form for lengths < 128; no leading zeros in long-form lengths).
Suggested fix (library):
Provide a strict mode helper that enforces canonical DER (including minimal length encoding) and exact consumption, or make parseDER optionally strict about trailing bytes / canonical length.