Skip to content

@fidm/asn1: parseDER accepts trailing bytes and non-canonical DER length encodings (ACCEPT_PARTIAL), enabling differential parsing / smuggling #4

@fasrm

Description

@fasrm

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions