CVE-2026-16381
Overview
Background
- `DNSPacket`
- Gecko’s networking class that parses raw DNS-over-HTTPS (DoH/TRR) response packets into resolved records.
- `DecodeInternal`
- the method on
DNSPacketthat walks an incoming DNS wire-format buffer and fills out addresses, a canonical name, and a typed result. - `aTypeResult`
- an out-parameter
mozilla::Variantholding a “type record” (e.g. anHTTPS/SVCBorTXTrecord) decoded from the packet, defaulting toNothing(). - Same-origin policy
- the browser rule that data resolved or fetched for one origin must not be exposed to a different origin.
Root Cause Analysis
The vulnerable path was DNSPacket::DecodeInternal, which is invoked to decode a DNS response into aTypeResult but never reset aTypeResult to Nothing() at entry. When the same DNSPacket/output slot was reused across decodes — or a later decode failed or contained no matching type record — the aTypeResult variant retained the type record accumulated by a previous decode. This violated the invariant that a decode’s output must reflect only the packet being parsed for the current request, so a stale record from one lookup could be surfaced as the answer for another.
The fix adds aTypeResult = mozilla::AsVariant(Nothing()); immediately after clearing aCname, guaranteeing the typed result starts empty on every call. Because the reset happens before any early-return error path (aLen < 12, etc.), no leftover record can survive a decode that does not legitimately produce one.
aTypeResult out-parameter at the start of DecodeInternal, letting a previous decode’s type record leak into an unrelated result; the fix forces aTypeResult back to Nothing() up front so each decode starts from a clean state.Attack Path
- Trigger a first resolution
The attacker’s origin causes a DNS/DoH lookup that decodes into a valid type record stored in
aTypeResult. - Reuse the decode path
A subsequent lookup reuses the same
DNSPacketoutput slot, decoding a response that carries no matching type record or hits an early return. - Stale record survives
Because
aTypeResultwas never reset, the previous decode’s type record remains in the variant instead ofNothing(). - Cross-origin exposure The stale type record is consumed as the answer for the second, different-origin request, bypassing the same-origin boundary between the two resolutions.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnetwerk/dns/DNSPacket.cpp |
modified |
Files Changed
netwerk/dns/DNSPacket.cpp
Audit Directions
- Uninitialized out-parametersAudit decode/parse routines whose callers reuse output buffers and confirm every out-parameter (especially
Variant/Maybe/Optionaltypes likeaTypeResult) is reset before any early-return path. - Cross-request state reuseReview
DNSPacketand sibling networking parsers for object or buffer reuse across resolutions where residual state from one request could leak into another. - Early-return leakageFor functions with multiple failure exits, verify that clearing/initialization occurs before the first
return, not after, so error paths cannot surface stale results.
Patch
diff --git a/netwerk/dns/DNSPacket.cpp b/netwerk/dns/DNSPacket.cpp
index 2fd99eab098..4578c7692b2 100644
--- a/netwerk/dns/DNSPacket.cpp
+++ b/netwerk/dns/DNSPacket.cpp
@@ -646,6 +646,9 @@ nsresult DNSPacket::DecodeInternal(
aCname.Truncate();
+ // Reset any type record accumulated by a previous decode of this packet.
+ aTypeResult = mozilla::AsVariant(Nothing());
+
if (aLen < 12) {
LOG(("TRR bad incoming DOH, eject!\n"));
return NS_ERROR_ILLEGAL_VALUE;