LibDNS: Reject domain name labels longer than 63 octets
A DNS label length octet is a 6-bit value, so a label can be at most 63 octets. Several code paths violated this and aborted the process: - DomainName::from_raw() treated any length octet that was not a compression pointer as an ordinary label, including the reserved 0b01/0b10 top-bit encodings, yielding labels of up to 191 octets. - DomainName::to_raw() then VERIFY-aborted on any label longer than 63 octets while encoding it back to the wire. - Resolver::lookup() encoded the outgoing query with MUST(to_raw(...)), so even once to_raw() fails gracefully the MUST would still abort. Together these crash RequestServer, which drives DNS for the whole browser. When a custom resolver is configured (e.g. --dns-server), a web page that references a host name containing a label longer than 63 characters aborts RequestServer while the query is encoded. A response carrying a reserved-length label is also accepted by the parser and then aborts when the name is re-encoded (for example during DNSSEC validation). Reject over-long labels when parsing, return an error instead of aborting when encoding, and reject the lookup promise instead of MUST-aborting at the query call site.
This commit is contained in:
parent
4ad7f06099
commit
f756219841
4 changed files with 55 additions and 2 deletions
|
|
@ -678,6 +678,11 @@ ErrorOr<DomainName> DomainName::from_raw(ParseContext& ctx)
|
|||
return Error::from_string_literal("Invalid domain name pointer in label");
|
||||
}
|
||||
|
||||
// A normal label is at most 63 octets. Lengths whose top two bits are 0b01 or 0b10 are reserved (RFC 1035,
|
||||
// 4.1.4) and would produce a label that can no longer be re-encoded, so reject them here.
|
||||
if (length > 63)
|
||||
return Error::from_string_literal("Domain name label exceeds 63 octets");
|
||||
|
||||
ByteBuffer content;
|
||||
TRY(ctx.stream.read_until_filled(TRY(content.get_bytes_for_writing(length))));
|
||||
name.labels.append(ByteString::copy(content));
|
||||
|
|
@ -691,7 +696,8 @@ ErrorOr<DomainName> DomainName::from_raw(ParseContext& ctx)
|
|||
ErrorOr<void> DomainName::to_raw(ByteBuffer& out) const
|
||||
{
|
||||
for (auto& label : labels) {
|
||||
VERIFY(label.length() <= 63);
|
||||
if (label.length() > 63)
|
||||
return Error::from_string_literal("Domain name label exceeds 63 octets");
|
||||
auto size_bytes = TRY(out.get_bytes_for_writing(1));
|
||||
u8 size = static_cast<u8>(label.length());
|
||||
memcpy(size_bytes.data(), &size, 1);
|
||||
|
|
|
|||
|
|
@ -666,7 +666,10 @@ public:
|
|||
});
|
||||
|
||||
ByteBuffer query_bytes;
|
||||
MUST(query.to_raw(query_bytes));
|
||||
if (auto result = query.to_raw(query_bytes); result.is_error()) {
|
||||
promise->reject(result.release_error());
|
||||
return promise;
|
||||
}
|
||||
|
||||
if (m_mode == ConnectionMode::TCP) {
|
||||
auto original_query_bytes = query_bytes;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
set(TEST_SOURCES
|
||||
TestDNSMessage.cpp
|
||||
TestDNSResolver.cpp
|
||||
)
|
||||
|
||||
|
|
|
|||
43
Tests/LibDNS/TestDNSMessage.cpp
Normal file
43
Tests/LibDNS/TestDNSMessage.cpp
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2026, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/MemoryStream.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibDNS/Message.h>
|
||||
#include <LibTest/TestCase.h>
|
||||
|
||||
// A label longer than 63 octets cannot be represented in the wire format, whose length field is a 6-bit value.
|
||||
// Encoding such a label used to abort the process via a VERIFY in DomainName::to_raw(); it must now fail
|
||||
// gracefully. Reachable from a query for a host name that contains an over-long label.
|
||||
TEST_CASE(encoding_a_domain_name_with_an_overlong_label_does_not_crash)
|
||||
{
|
||||
auto domain_name = DNS::Messages::DomainName::from_string(ByteString::repeated('a', 64));
|
||||
|
||||
ByteBuffer out;
|
||||
auto result = domain_name.to_raw(out);
|
||||
EXPECT(result.is_error());
|
||||
}
|
||||
|
||||
// A label length octet whose top two bits are 0b01 or 0b10 is reserved (RFC 1035, 4.1.4); the parser used to
|
||||
// accept it as an ordinary label of up to 191 octets, producing a name that could no longer be re-encoded.
|
||||
// Parsing such a message must fail instead.
|
||||
TEST_CASE(parsing_a_reserved_label_length_fails)
|
||||
{
|
||||
// Header with QDCOUNT = 1 and all other counts 0, then a question whose name starts with a label length octet
|
||||
// of 0x40 (reserved top bits, 64 octets).
|
||||
Vector<u8> message;
|
||||
message.extend({ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 });
|
||||
message.append(0x40);
|
||||
for (size_t i = 0; i < 64; ++i)
|
||||
message.append('a');
|
||||
message.append(0x00);
|
||||
message.extend({ 0, 1, 0, 1 }); // QTYPE = A, QCLASS = IN
|
||||
|
||||
FixedMemoryStream stream { message.span() };
|
||||
auto result = DNS::Messages::Message::from_raw(stream);
|
||||
EXPECT(result.is_error());
|
||||
}
|
||||
Loading…
Reference in a new issue