CVE-2026-16406
Overview
Background
- SVCB/HTTPS record
- a DNS resource record type that carries service connection parameters, including
ipv4hintandipv6hintfields that suggest addresses for the origin. - `ParseSvcParam`
- the
DNSPacketstatic method that decodes an individualSvcParamKeyvalue such as an IP hint into aSvcFieldValue. - RFC 1918 address
- an IP address in a private/local range (e.g.
10.0.0.0/8,192.168.0.0/16) that should normally not be reachable from a public web origin. - `IsIPAddrLocal`
- a
NetAddrpredicate that reports whether an address falls in a private/local range.
Root Cause Analysis
When parsing SVCB/HTTPS DNS answers, DNSPacket::ParseSvcParam unconditionally appended every ipv4hint and ipv6hint address into ipv4array/ipv6array via AppendElement, with no check against the local-address filtering that Firefox applies elsewhere to DNS results. This meant a malicious or upstream-poisoned resolver could return HTTPS-record IP hints pointing at RFC 1918 / local addresses, bypassing the mitigation that keeps public-origin name resolution from resolving to internal hosts.
The fix threads a bool aAllowRFC1918 flag from DecodeInternal through ParseHTTPS into ParseSvcParam, and gates each AppendElement behind if (aAllowRFC1918 || !addr.IsIPAddrLocal()), so local addresses are dropped when the caller disallows them.
The patch also corrects aParsed.mHasIPHints to be set only when a hint value is non-empty, so a hint list emptied entirely by the new filter no longer marks the record as having usable IP hints. Together this restores the invariant that a public origin cannot be steered to a private/local IP through the SVCB IP-hint path.
ipv4hint/ipv6hint parsing path never applied the RFC 1918 local-address filter that guards other DNS results, so it was a hole in that mitigation; the fix propagates the existing aAllowRFC1918 policy down into ParseSvcParam and filters local hints at append time.Attack Path
- Control a DNS answer An attacker who controls or can spoof the resolver response for an origin returns an HTTPS/SVCB record for that origin.
- Embed local IP hints
The record’s
ipv4hint/ipv6hintfields carry RFC 1918 or otherwise local addresses. - Unfiltered parse
ParseSvcParamappends those local addresses into the hint arrays without anyIsIPAddrLocalcheck, andmHasIPHintsis set. - Local address used Firefox treats the public origin as resolving to the attacker-chosen internal address, bypassing the local-address mitigation.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchnetwerk/dns/DNSPacket.cpp |
modified | |
ifnetwerk/dns/DNSPacket.cpp |
modified | |
fornetwerk/dns/DNSPacket.cpp |
modified |
Files Changed
netwerk/dns/DNSPacket.cppnetwerk/dns/DNSPacket.h
Audit Directions
- Mitigation policy propagationCheck every DNS record type and parser (A, AAAA, SVCB, HTTPS) that produces addresses to confirm the
aAllowRFC1918/local-address filter is applied uniformly and not dropped along any call chain. - Empty-collection state flagsLook for boolean “has X” flags (like
mHasIPHints) set before a collection is filtered, where later filtering can empty the collection but leave the flag stale. - `AppendElement` on parsed network dataAudit places that append attacker-influenced addresses or values into result arrays without validating them against the surrounding security policy.
Patch
diff --git a/netwerk/dns/DNSPacket.cpp b/netwerk/dns/DNSPacket.cpp
index 46a18a6c09f..2fd99eab098 100644
--- a/netwerk/dns/DNSPacket.cpp
+++ b/netwerk/dns/DNSPacket.cpp
@@ -67,7 +67,8 @@ nsresult DNSPacket::FillBuffer(
// static
nsresult DNSPacket::ParseSvcParam(unsigned int svcbIndex, uint16_t key,
SvcFieldValue& field, uint16_t length,
- const unsigned char* aBuffer) {
+ const unsigned char* aBuffer,
+ bool aAllowRFC1918) {
switch (key) {
case SvcParamKeyMandatory: {
if (length % 2 != 0) {
@@ -134,7 +135,9 @@ nsresult DNSPacket::ParseSvcParam(unsigned int svcbIndex, uint16_t key,
addr.inet.family = AF_INET;
addr.inet.port = 0;
addr.inet.ip = ntohl(get32bit(aBuffer, svcbIndex));
- ipv4array.AppendElement(addr);
+ if (aAllowRFC1918 || !addr.IsIPAddrLocal()) {
+ ipv4array.AppendElement(addr);
+ }
length -= 4;
svcbIndex += 4;
}
@@ -162,7 +165,9 @@ nsresult DNSPacket::ParseSvcParam(unsigned int svcbIndex, uint16_t key,
for (int i = 0; i < 16; i++, svcbIndex++) {
addr.inet6.ip.u8[i] = aBuffer[svcbIndex];
}
- ipv6array.AppendElement(addr);
+ if (aAllowRFC1918 || !addr.IsIPAddrLocal()) {
+ ipv6array.AppendElement(addr);
+ }
length -= 16;
// no need to increase svcbIndex - we did it in the for above.
}
@@ -498,7 +503,8 @@ nsresult DNSPacket::ParseHTTPS(uint16_t aRDLen, struct SVCB& aParsed,
unsigned int aIndex,
const unsigned char* aBuffer,
unsigned int aBodySize,
- const nsACString& aOriginHost) {
+ const nsACString& aOriginHost,
+ bool aAllowRFC1918) {
int32_t lastSvcParamKey = -1;
nsresult rv = NS_OK;
unsigned int svcbIndex = aIndex;
@@ -562,7 +568,7 @@ nsresult DNSPacket::ParseHTTPS(uint16_t aRDLen, struct SVCB& aParsed,
return NS_ERROR_UNEXPECTED;
}
- rv = ParseSvcParam(svcbIndex, key, value, len, aBuffer);
+ rv = ParseSvcParam(svcbIndex, key, value, len, aBuffer, aAllowRFC1918);
if (NS_FAILED(rv)) {
return rv;
}
@@ -574,8 +580,12 @@ nsresult DNSPacket::ParseHTTPS(uint16_t aRDLen, struct SVCB& aParsed,
continue;
}
- if (value.mValue.is<SvcParamIpv4Hint>() ||
- value.mValue.is<SvcParamIpv6Hint>()) {
+ if (value.mValue.is<SvcParamIpv4Hint>() &&
+ !value.mValue.as<SvcParamIpv4Hint>().mValue.IsEmpty()) {
+ aParsed.mHasIPHints = true;
+ }
+ if (value.mValue.is<SvcParamIpv6Hint>() &&
+ !value.mValue.as<SvcParamIpv6Hint>().mValue.IsEmpty()) {
aParsed.mHasIPHints = true;
}
if (value.mValue.is<SvcParamEchConfig>()) {
@@ -847,7 +857,7 @@ nsresult DNSPacket::DecodeInternal(
}
rv = ParseHTTPS(RDLENGTH, parsed, index, aBuffer, mBodySize,
- mOriginHost ? *mOriginHost : qname);
+ mOriginHost ? *mOriginHost : qname, aAllowRFC1918);
if (NS_FAILED(rv)) {
return rv;
}
diff --git a/netwerk/dns/DNSPacket.h b/netwerk/dns/DNSPacket.h
index 16c44d1e8c8..0d0e404383f 100644
--- a/netwerk/dns/DNSPacket.h
+++ b/netwerk/dns/DNSPacket.h
@@ -74,7 +74,8 @@ class DNSPacket {
static nsresult ParseHTTPS(uint16_t aRDLen, struct SVCB& aParsed,
unsigned int aIndex, const unsigned char* aBuffer,
unsigned int aBodySize,
- const nsACString& aOriginHost);
+ const nsACString& aOriginHost,
+ bool aAllowRFC1918 = true);
void SetNativePacket(bool aNative) { mNativePacket = aNative; }
static nsresult GetQname(nsACString& aQname, unsigned int& aIndex,
@@ -85,7 +86,8 @@ class DNSPacket {
nsresult PassQName(unsigned int& index, const unsigned char* aBuffer);
static nsresult ParseSvcParam(unsigned int svcbIndex, uint16_t key,
SvcFieldValue& field, uint16_t length,
- const unsigned char* aBuffer);
+ const unsigned char* aBuffer,
+ bool aAllowRFC1918 = true);
nsresult DecodeInternal(
nsCString& aHost, enum TrrType aType, nsCString& aCname,
bool aAllowRFC1918, DOHresp& aResp, TypeRecordResultType& aTypeResult,