Skip to content

BER indefinite-length parser should throw on missing end-of-contents octets instead of silently dropping last child #125

Description

@sour-exploit

Summary

When parsing a BER indefinite-length constructed type whose contents are truncated before the mandatory end-of-contents (EOC) octets 00 00, the parser silently drops the last child element instead of throwing an error.

The loop at ASN1.swift:258-263:

let lastIndex = nodes.endIndex - 1
repeat {
    try _parseNode(from: &data, encoding: rules, depth: depth + 1, into: &nodes)
} while data.count > 0 && nodes.last!.isEndMarker == false
let endMarker = nodes.popLast()!
let encodedBytes = originalData[..<endMarker.encodedBytes.endIndex]
nodes[lastIndex].encodedBytes = encodedBytes

The loop exits in two ways:

  1. nodes.last!.isEndMarker == true - an EOC marker was found (correct behavior).
  2. data.count == 0 - data is exhausted before any EOC appears (truncated input).
    In case 2, nodes.popLast()! removes the last legitimately parsed child and treats it as the EOC marker, silently dropping it from the result. The parent node's encodedBytes is then set to an incorrect range derived from the dropped child's position.

Expected behavior

When the input is exhausted without encountering an EOC marker, the parser should throw an ASN1Error indicating truncated or malformed input, per X.690 §8.1.3.6.1 which requires end-of-contents octets to terminate all indefinite-length encodings.

Reproducer

import XCTest
import SwiftASN1
 
final class BERIndefiniteLengthTruncationTests: XCTestCase {
    func testTruncatedIndefiniteLengthDropsChild() throws {
        // SEQUENCE (indefinite), one OCTET STRING child 'A', NO end-of-contents marker.
        let bytes: [UInt8] = [0x30, 0x80, 0x04, 0x01, 0x41]
 
        // Currently: parses without error, but the SEQUENCE has zero children
        // (the OCTET STRING was silently popped as a fake EOC marker).
        // Expected: should throw ASN1Error indicating missing EOC.
        let parsed = try? BER.parse(bytes)
 
        if let parsed = parsed {
            var iterator = parsed.makeIterator()
            // The child that was parsed (OCTET STRING) should be present:
            XCTAssertNotNil(iterator.next(), "Child was silently dropped — data loss on truncated BER")
        }
    }
 
    func testTruncatedIndefiniteLengthShouldThrow() {
        let bytes: [UInt8] = [0x30, 0x80, 0x04, 0x01, 0x41]
        XCTAssertThrowsError(try BER.parse(bytes),
            "Truncated indefinite-length input should throw, not silently drop data")
    }
}

Proposed fix

After the loop, check whether the exit was due to data exhaustion rather than an EOC marker:

repeat {
    try _parseNode(from: &data, encoding: rules, depth: depth + 1, into: &nodes)
} while data.count > 0 && nodes.last!.isEndMarker == false
 
guard nodes.last!.isEndMarker else {
    throw ASN1Error.truncatedASN1Field(
        reason: "Indefinite-length constructed type is missing end-of-contents octets"
    )
}
 
let endMarker = nodes.popLast()!
// ... rest unchanged

Context

This code was introduced in commit 28d0e07 ("Add support for parsing BER") and was not touched by the CVE-2025-0343 fix (commit ae33e59, which hardened DER.swift and per-type decoders). This is a correctness issue, not a crash or memory-safety bug.

References

  • X.690 §8.1.3.6.1 (end-of-contents octets requirement for indefinite-length)
  • RFC 8825 §3 (BER encoding rules)

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