Skip to content

Dnskey::parse bypasses 16-bit RDATA length invariant via new_unchecked #621

@yilin0518

Description

@yilin0518

I found a safe-path invariant violation in domain 0.11.1 around DNSKEY parsing.

Summary

Dnskey::new enforces that wire-format RDATA length is at most 65535 bytes via LongRecordData::check_len(...).
However, Dnskey::parse does not enforce the same bound and directly calls unsafe { Dnskey::new_unchecked(...) } with parser.remaining() - 4 bytes.

This allows constructing a Dnskey with total wire length > 65535 using only safe APIs.

Poc:

use domain::rdata::dnssec::Dnskey;
use octseq::Parser;

fn main() {
    // Build a DNSKEY-like blob larger than the 16-bit DNS RDATA limit.
    let mut buf = vec![0u8; 70_000];
    buf[2] = 3;
    buf[3] = 8;

    let mut parser = Parser::from_ref(&buf[..]);

    // Safe parse accepts the oversized payload.
    let dnskey = Dnskey::parse(&mut parser).expect("Parse should succeed");

    let total_wire_length = 4 + dnskey.into_public_key().as_ref().len();
    println!("total wire length: {total_wire_length}");

    // This should not be possible if the constructor invariant were preserved.
    assert!(total_wire_length > 65_535);
}

Observed output:

Image

It breaks the documented safety precondition of new_unchecked through a safe public API path, creating a soundness-risk and making downstream assumptions about DNS RDATA size unreliable.

Suggested fix:

Before calling new_unchecked in Dnskey::parse, validate total RDATA length using the same check as Dnskey::new (or just construct via Dnskey::new after parsing fields).

Optionally, audit similar parse -> new_unchecked paths in other record types for the same invariant bypass pattern.

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