Firefox reuses pre-CNAME HTTPS/SVCB ServiceMode metadata across CNAME chains, exposing protected same-origin backends
Categories
(Core :: Networking: DNS, defect, P2)
Tracking
()
People
(Reporter: rintaroumelon, Assigned: valentin)
Details
(Keywords: csectype-sop, reporter-external, sec-moderate, Whiteboard: [client-bounty-form][necko-triaged][necko-priority-next][adv-main153+][adv-ESR140.13+])
Attachments
(4 files)
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-esr140+
|
Details | Review |
summary
Runtime verified on Firefox Nightly 152.0a1, BuildID 20260518093736. The attacker-controlled input is an HTTPS/SVCB DNS answer returned through Firefox TRR for a hostname resolved by the browser. The external entry point is normal browser navigation or subresource loading for that hostname while Firefox uses the attacker-controlled TRR response.
An initial single-CNAME scenario using ALIAS_HOST CNAME CANONICAL_HOST did not reproduce ALT_PORT selection under the default network.dns.https_rr.check_record_with_cname=true behavior; Firefox logged the CNAME follow and then no usable record!. A follow-up runtime scenario reproduced the browser-level issue by exercising an address-side CNAME chain. The DoH server returned ALIAS_HOST CNAME MID_HOST, MID_HOST CNAME CANONICAL_HOST, stale same-owner ALIAS_HOST HTTPS 1 CANONICAL_HOST alpn=h2 port=ALT_PORT, and canonical CANONICAL_HOST HTTPS 2 CANONICAL_HOST alpn=h2 port=CANONICAL_PORT. With the default CNAME check enabled, Firefox selected the stale alias-owned ALT_PORT route, delivered same-origin HTTP/2 requests to the stale backend, accepted the response, and sent the cookie set by that backend on the later /secret navigation. A high-impact recheck modeled CANONICAL_PORT as a public backend and ALT_PORT as a protected admin backend for the same accepted TLS origin. In 50/50 positive iterations, Firefox selected the stale ALT_PORT route, browser JavaScript read admin-secret: STALE_ALT_ADMIN_ONLY, and the admin backend state was mutated. Controls without the stale record, with a mismatching stale TargetName, with direct canonical navigation, and with an unsafe alternate port routed to the canonical endpoint and did not read or mutate the admin backend; disabling the CNAME check routed the mismatch control to the alternate endpoint as expected.
In netwerk/dns/TRR.cpp, TRR::FollowCname follows a DNS CNAME by decoding the same DNS packet again for the canonical name, but it passes the same persistent mResult object into the second decode. TRR::HasUsableResponse then treats any TypeRecordHTTPSSVC result as usable without proving that the HTTPS records came from the post-CNAME owner.
bool TRR::HasUsableResponse() {
if (mType == TRRTYPE_A || mType == TRRTYPE_AAAA) {
return !mDNS.mAddresses.IsEmpty();
}
if (mType == TRRTYPE_TXT) {
return mResult.is<TypeRecordTxt>();
}
if (mType == TRRTYPE_HTTPSSVC) {
return mResult.is<TypeRecordHTTPSSVC>();
}
return false;
}
nsresult TRR::FollowCname(nsIChannel* aChannel) {
nsresult rv = NS_OK;
nsAutoCString cname;
while (NS_SUCCEEDED(rv) && mDNS.mAddresses.IsEmpty() && !mCname.IsEmpty() &&
mCnameLoop > 0) {
mCnameLoop--;
LOG(("TRR::On200Response CNAME %s => %s (%u)\n", mHost.get(), mCname.get(),
mCnameLoop));
cname = mCname;
mCname.Truncate();
LOG(("TRR: check for CNAME record for %s within previous response\n",
cname.get()));
nsClassHashtable<nsCStringHashKey, DOHresp> additionalRecords;
rv = GetOrCreateDNSPacket()->Decode(
cname, mType, mCname, StaticPrefs::network_trr_allow_rfc1918(), mDNS,
mResult, additionalRecords, mTTL);
In netwerk/dns/DNSPacket.cpp, DNSPacket::DecodeInternal clears aCname at the start of each decode, but it does not clear aTypeResult. A first decode for the original owner can therefore append an HTTPS ServiceMode record to mResult, and the second decode for the CNAME target can leave that earlier record in place.
LOG(("doh decode %s %d bytes\n", aHost.get(), aLen));
aCname.Truncate();
if (aLen < 12) {
LOG(("TRR bad incoming DOH, eject!\n"));
return NS_ERROR_ILLEGAL_VALUE;
}
In netwerk/dns/DNSPacket.cpp, DNSPacket::DecodeInternal, HTTPS ServiceMode records matching the current decode owner are appended to the caller-provided aTypeResult. The code clears aTypeResult only for HTTPS AliasMode, not for DNS CNAME processing.
aTTL = TTL;
if (!aTypeResult.is<TypeRecordHTTPSSVC>()) {
aTypeResult = mozilla::AsVariant(CopyableTArray<SVCB>());
}
{
auto& results = aTypeResult.as<TypeRecordHTTPSSVC>();
results.AppendElement(parsed);
}
break;
}
In netwerk/dns/DNSPacket.cpp, DNSPacket::DecodeInternal, the no-result check is also bypassed after stale ServiceMode data exists, because the old TypeRecordHTTPSSVC value means aTypeResult.is<TypeRecordEmpty>() is false during the post-CNAME decode.
if ((aType != TRRTYPE_NS) && aCname.IsEmpty() && aResp.mAddresses.IsEmpty() &&
aTypeResult.is<TypeRecordEmpty>()) {
// no entries were stored!
LOG(("TRR: No entries were stored!\n"));
In netwerk/dns/HTTPSSVC.cpp, the CNAME usability check compares only the ServiceMode TargetName with the DNS CNAME. The SVCB object does not carry the RR owner name, so a stale ServiceMode record originally owned by the pre-CNAME name can pass when its TargetName equals the CNAME.
static bool CheckRecordIsUsableWithCname(const SVCB& aRecord,
const nsACString& aCname) {
if (StaticPrefs::network_dns_https_rr_check_record_with_cname() &&
!aCname.IsEmpty() && !aRecord.mSvcDomainName.Equals(aCname)) {
return false;
}
return true;
}
The missing check is owner separation when a DNS CNAME is followed. Once a DNS CNAME is accepted, HTTPS/SVCB ServiceMode records owned by the original alias should not remain eligible as service candidates for the canonical owner. Existing unsafe-port, ALPN, and TargetName checks do not remove the stale record if the attacker chooses a safe port, supported ALPN, and TargetName equal to the CNAME.
poc
- Use a Firefox build from the affected source. Configure Firefox to use an attacker-controlled TRR endpoint and force TRR resolution for the test profile.
- Configure the TRR endpoint so address resolution follows a CNAME chain:
ALIAS_HOST. TTL IN CNAME MID_HOST.
MID_HOST. TTL IN CNAME CANONICAL_HOST.
CANONICAL_HOST. TTL IN A CANONICAL_ADDR
- Configure the HTTPS query for
ALIAS_HOSTso the same response contains the alias CNAME, a stale same-owner ServiceMode record, and the canonical ServiceMode record:
ALIAS_HOST. TTL IN CNAME MID_HOST.
ALIAS_HOST. TTL IN HTTPS 1 CANONICAL_HOST. alpn=h2 port=ALT_PORT
CANONICAL_HOST. TTL IN HTTPS 2 CANONICAL_HOST. alpn=h2 port=CANONICAL_PORT
- Run two TLS-capable HTTP/2 endpoints: a public/canonical backend on
CANONICAL_HOST:CANONICAL_PORTand a protected admin backend onCANONICAL_HOST:ALT_PORT. Use a certificate that Firefox accepts for the originALIAS_HOST. - Trigger the real browser entry point so a same-origin page fetches an admin-only endpoint and mutates state:
import webbrowser
webbrowser.open("https://ALIAS_HOST/start?scenario=positive_stale")
- The verified vulnerable result is that Firefox connects to
CANONICAL_HOST:ALT_PORT, the port from the stale same-ownerALIAS_HOST HTTPSServiceMode record, rather than selecting only the HTTPS data owned byCANONICAL_HOST. - In the Nightly 152.0a1 high-impact run, the positive scenario completed 50/50 iterations. The stale admin endpoint on
ALT_PORTreceived 150 requests, browser JavaScript read the admin-only response body 50 times, and/admin-secret?mutate=1incremented admin state 50 times. Follow-up requests carried the stale backend cookie. - Controls confirmed attribution: removing the stale alias-owned ServiceMode record routed to
CANONICAL_PORT; using a mismatching stale TargetName routed toCANONICAL_PORTwith the default pref; turning the CNAME check off routed the mismatch case toALT_PORT; direct canonical navigation routed toCANONICAL_PORT; an unsafe alternate port was filtered and routed toCANONICAL_PORT.
impact
An attacker who can supply malformed HTTPS/SVCB DNS answers through the TRR path can make Firefox select ServiceMode metadata from the pre-CNAME owner after the lookup has moved to the canonical name. The reproduced impact is browser-level same-origin backend authority confusion: a same-origin request, including cookies set during the session, can be routed to a protected admin backend selected from stale alias-owned HTTPS metadata rather than the canonical owner’s public backend. Firefox accepted the stale backend's response for the original origin, browser JavaScript read the admin-only response body, and the stale backend processed a state-changing same-origin request when TLS origin validation succeeded.
TLS origin validation and unsafe-port filtering still constrain exploitation, and the issue is strongest when the same accepted origin fronts backends with different privilege. The high-impact recheck showed deterministic selection under the malformed TRR answer without DNS RR ordering or timing changes, so the base score uses AC:L.
CWE: CWE-436 - Interpretation Conflict
CVSS: 7.1 (High) - CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N
Updated•4 months ago
|
Updated•4 months ago
|
Comment 1•3 months ago
|
||
The rating assigned by our bug rating tool (currently under development) is:
Rating: sec-moderate
Explanation: The bug is valid: TRR::FollowCname passes the same mResult object across CNAME hops without clearing HTTPS/SVCB ServiceMode records, and CheckRecordIsUsableWithCname only validates the TargetName against the CNAME (not the RR owner), allowing pre-CNAME stale records to survive and route requests to alternate ports. However, exploitation requires attacker control of the user's TRR/DoH endpoint (a significant precondition limiting affected users), and TLS validation still constrains the attack to same-TLS-origin scenarios where the target has privileged backends on different ports under the same certificate. The vulnerability would be sec-high without these constraints, but the TRR control requirement and TLS constraint make it sec-moderate.
Kershaw: Please ni? me if you disagree with this rating.
| Assignee | ||
Updated•3 months ago
|
| Assignee | ||
Comment 2•3 months ago
|
||
| Assignee | ||
Comment 3•3 months ago
|
||
Comment 4•3 months ago
|
||
(In reply to Tyson Smith [:tsmith] from comment #1)
The rating assigned by our bug rating tool (currently under development) is:
Rating: sec-moderate
Explanation: The bug is valid: TRR::FollowCname passes the same mResult object across CNAME hops without clearing HTTPS/SVCB ServiceMode records, and CheckRecordIsUsableWithCname only validates the TargetName against the CNAME (not the RR owner), allowing pre-CNAME stale records to survive and route requests to alternate ports. However, exploitation requires attacker control of the user's TRR/DoH endpoint (a significant precondition limiting affected users), and TLS validation still constrains the attack to same-TLS-origin scenarios where the target has privileged backends on different ports under the same certificate. The vulnerability would be sec-high without these constraints, but the TRR control requirement and TLS constraint make it sec-moderate.
Kershaw: Please ni? me if you disagree with this rating.
I agree the sec rating and thanks Valentin for preparing a fix.
Comment 6•3 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: An attacker controlling the DoH server could cause Firefox to connect to a port specified by the intermediary HTTPS record instead the one of the final one.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: no
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Low. Enforces correctness.
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 7•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D308991
Comment 8•3 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: An attacker controlling the DoH server could cause Firefox to connect to a port specified by the intermediary HTTPS record instead the one of the final one.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: no
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Low. Enforces correctness.
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 9•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D308991
Comment 10•3 months ago
|
||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 11•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Comment 12•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•2 months ago
|
Updated•2 months ago
|
Updated•26 days ago
|
Description
•