Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionMitigation bypass in the Networking component
ComponentNetworking
Bug ClassLogic Error
Tracker2040382
Fix commit754a90a28f3d (firefox) +22/-10
CISA KEVNot listed
CreditedRintaro Kawasugi
Disclosed2026-07-21

Background

SVCB/HTTPS record
a DNS resource record type that carries service connection parameters, including ipv4hint and ipv6hint fields that suggest addresses for the origin.
`ParseSvcParam`
the DNSPacket static method that decodes an individual SvcParamKey value such as an IP hint into a SvcFieldValue.
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 NetAddr predicate 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.

Key insight
The single mistake was that the SVCB 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

  1. 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.
  2. Embed local IP hints The record’s ipv4hint/ipv6hint fields carry RFC 1918 or otherwise local addresses.
  3. Unfiltered parse ParseSvcParam appends those local addresses into the hint arrays without any IsIPAddrLocal check, and mHasIPHints is set.
  4. Local address used Firefox treats the public origin as resolving to the attacker-chosen internal address, bypassing the local-address mitigation.

Impact Assessment

An attacker gains the ability to make a public web origin resolve to a private/local (RFC 1918) IP via SVCB/HTTPS IP hints, undermining the mitigation meant to prevent public names from reaching internal hosts. This occurs in the networking/DNS parsing path of the browser process, and requires the attacker to control or spoof the SVCB/HTTPS DNS answer for the targeted origin. Per the metadata this is a low-severity mitigation bypass, not a memory-safety or direct-code-execution issue.

Changed Functions

FunctionChangeNotes
switch
netwerk/dns/DNSPacket.cpp
modified
if
netwerk/dns/DNSPacket.cpp
modified
for
netwerk/dns/DNSPacket.cpp
modified

Files Changed

  • netwerk/dns/DNSPacket.cpp
  • netwerk/dns/DNSPacket.h

Audit Directions

  • Mitigation policy propagation
    Check 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 flags
    Look 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 data
    Audit places that append attacker-influenced addresses or values into result arrays without validating them against the surrounding security policy.
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,
Loading diff…