Closed Bug 2040697 Opened 5 months ago Closed 3 months ago

Firefox Alt-Svc reuse delivers origin requests to Private endpoint without LNA permission

Categories

(Core :: Networking, defect, P1)

defect

Tracking

()

RESOLVED FIXED
154 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 --- unaffected
firefox-esr153 --- fixed
firefox152 --- wontfix
firefox153 --- fixed
firefox154 + fixed

People

(Reporter: rintaroumelon, Assigned: kershaw)

References

(Blocks 1 open bug, Regression)

Details

(4 keywords, Whiteboard: [client-bounty-form][necko-triaged][necko-priority-queue][adv-main153-])

Attachments

(1 external file)

summary

Runtime verified on Firefox Nightly 152.0a1, BuildID 20260518093736; no fixed version or tag is known. The attacker-controlled input is an Alt-Svc response header from an HTTPS origin and the DNS answer for the advertised alternate host. The external entry point is normal browsing to that HTTPS origin, followed by same-origin navigation or subresource loading after Firefox has validated the attacker-supplied Alt-Svc mapping.

In netwerk/protocol/http/AlternateServices.cpp, AltSvcCache::GetAltServiceMapping returns a cached HTTP/2 or HTTP/3 mapping only after that mapping is marked validated.

  if (aHttp2Allowed) {
    nsAutoCString key;
    AltSvcMapping::MakeHashKey(key, scheme, host, port, privateBrowsing,
                               originAttributes, false);
    RefPtr<AltSvcMapping> existing = LookupMapping(key, privateBrowsing);
    LOG(
        ("AltSvcCache::GetAltServiceMapping %p key=%s "
         "existing=%p validated=%d ttl=%d",
         this, key.get(), existing.get(), existing ? existing->Validated() : 0,
         existing ? existing->TTL() : 0));
    if (existing && existing->Validated()) {
      return existing.forget();
    }
  }

In netwerk/protocol/http/nsHttpChannel.cpp, the channel accepts a validated Alt-Svc mapping for an ordinary HTTP transaction and replaces mConnectionInfo with connection info built from the alternate host and port.

  RefPtr<AltSvcMapping> mapping;
  if (!mConnectionInfo && LoadAllowAltSvc() &&  // per channel
      !mWebTransportSessionEventListener && (http2Allowed || http3Allowed) &&
      !(mLoadFlags & LOAD_FRESH_CONNECTION) &&
      AltSvcMapping::AcceptableProxy(proxyInfo) &&
      (scheme.EqualsLiteral("http") || scheme.EqualsLiteral("https")) &&
      (mapping = gHttpHandler->GetAltServiceMapping(
           scheme, host, port, mPrivateBrowsing, originAttributes, http2Allowed,
           http3Allowed))) {

In netwerk/protocol/http/nsHttpChannel.cpp, the same workflow then calls AltSvcMapping::GetConnectionInfo and uses the alternate route for the real channel. The quoted path does not include an LNA permission check for the Private address selected by the alternate hostname.

    LOG(("nsHttpChannel %p Using connection info from altsvc mapping", this));
    mapping->GetConnectionInfo(getter_AddRefs(mConnectionInfo), proxyInfo,
                               originAttributes);
    glean::http::transaction_use_altsvc
        .EnumGet(glean::http::TransactionUseAltsvcLabel::eTrue)
        .Add();

In netwerk/protocol/http/AlternateServices.cpp, AltSvcMapping::GetConnectionInfo preserves the origin host and port while inserting the alternate host and port into the connection information. This lets a request for ORIGIN_HOST be dispatched over a connection to ALT_HOST:ALT_PORT once the mapping is selected.

void AltSvcMapping::GetConnectionInfo(
    nsHttpConnectionInfo** outCI, nsProxyInfo* pi,
    const OriginAttributes& originAttributes) {
  RefPtr<nsHttpConnectionInfo> ci = new nsHttpConnectionInfo(
      mOriginHost, mOriginPort, mNPNToken, mUsername, pi, originAttributes,
      mAlternateHost, mAlternatePort, mIsHttp3, false);

The missing check is LNA enforcement when a validated Alt-Svc mapping is reused for a real origin transaction. TLS authentication confirms that the alternate endpoint is acceptable for the origin, but it does not decide whether a public-address context may send same-origin HTTP requests to a Private address selected by attacker-controlled Alt-Svc and DNS data.

The runtime PoC confirmed request delivery beyond validation-only probing. The origin returned Alt-Svc: h2="ALT_HOST:ALT_PORT"; ma=86400; after validation, the public origin connection path was made unavailable and same-origin requests were triggered from the already loaded page. Firefox delivered HTTP/2 HEADERS to PRIVATE_ADDR:ALT_PORT with :authority: ORIGIN_HOST, alt-used: ALT_HOST:ALT_PORT, and origin cookies. The private endpoint received GET /mutate, POST /mutate, and GET /reload; the JavaScript caller read the private endpoint's response bodies for both mutate requests, and the private endpoint state counter advanced to 2. The reload response was accepted as an ORIGIN_HOST document with title PRIVATE_ALT_DOC.

poc

  1. Use a Firefox build from the affected source with Alt-Svc enabled and Local Network Access blocking enabled.
  2. Serve https://ORIGIN_HOST/ with an Alt-Svc header pointing at an attacker-controlled alternate hostname:
HTTP/1.1 200 OK
Alt-Svc: h2="ALT_HOST:ALT_PORT"; ma=86400
Content-Length: 0
  1. Configure DNS so ALT_HOST resolves to a Private address where a TLS-capable HTTP/2 listener records real requests.
  2. Trigger the real browser entry point and set an origin cookie:
import webbrowser
webbrowser.open("https://ORIGIN_HOST/")
  1. Wait for Alt-Svc validation to complete, then close or make unavailable the public origin connection path.
  2. From the existing page, trigger same-origin requests:
await fetch("https://ORIGIN_HOST/mutate?label=get", {
  cache: "no-store",
  credentials: "include"
});
await fetch("https://ORIGIN_HOST/mutate?label=post", {
  method: "POST",
  cache: "no-store",
  credentials: "include"
});
location.href = "https://ORIGIN_HOST/reload";
  1. The verified vulnerable result is that the listener for ALT_HOST:ALT_PORT observes cookie-bearing HTTP/2 request HEADERS for the origin, not just validation frames. In the Nightly 152.0a1 run, the private endpoint saw :authority: ORIGIN_HOST, alt-used: ALT_HOST:ALT_PORT, and origin cookies on GET /mutate, POST /mutate, and GET /reload.
  2. The JavaScript caller read the private endpoint's response body for both mutate requests, the private endpoint state counter advanced to 2, and the reload response was accepted as a document for ORIGIN_HOST.
  3. Run controls: no Alt-Svc header, Alt-Svc disabled, public alternate host, validation-only frame inspection, and ordinary public-to-private fetch with LNA blocking. The verified control matrix observed no private validation connection in the no-header or Alt-Svc-disabled controls, normal validation for the public alternate host, HTTP/2 SETTINGS/WINDOW_UPDATE but no HEADERS during validation-only inspection, and LNA blocking for the ordinary fetch control.
  4. These observations prove this issue because the browser consumed attacker-supplied Alt-Svc and DNS data to validate a Private alternate endpoint and then reused that route for real same-origin HTTP traffic without an LNA permission grant.

impact

An attacker-controlled HTTPS origin can advertise an Alt-Svc endpoint whose hostname resolves to a Private address and cause Firefox to deliver same-origin HTTP/2 requests to that private endpoint without an LNA permission decision. The verified impact is cookie-bearing request delivery across the intended public-to-private LNA boundary: JavaScript read the private endpoint's responses as same-origin responses, and the private endpoint processed state-changing requests.

Flags: sec-bounty?
Group: firefox-core-security → network-core-security
Component: Security → Networking
Product: Firefox → Core
Severity: -- → S3
Priority: -- → P1
Whiteboard: [client-bounty-form] → [client-bounty-form][necko-triaged][necko-priority-queue]

The rating assigned by our bug rating tool (currently under development) is:

Rating: sec-moderate

Explanation: The bug describes a real bypass of the Local Network Access (LNA) security boundary via Alt-Svc header reuse. The Alt-Svc validation path uses SpeculativeTransaction, whose AllowedToConnectToIpAddressSpace inherits the base nsAHttpTransaction default that unconditionally returns true, so the validation connection to a private endpoint bypasses LNA. If TLS validation succeeds and the mapping is marked validated, subsequent real requests may be routed to the private endpoint. The runtime-verified impact (cookie-bearing requests, response body reading) is significant (cross-origin data exfiltration from private network). However, full exploitation requires the private endpoint to present a TLS certificate valid for ORIGIN_HOST — a certificate the attacker must control on a device in the victim's private network, which is an unusual and complex condition limiting real-world scope. Without this TLS constraint, the impact would be sec-high (cross-origin private network data leak), but the cert requirement constrains this to sec-moderate per the guidance on vulnerabilities limited in scope due to unusual conditions.

Sunil: Please ni? me if you disagree with this rating.

Flags: needinfo?(smayya)
Keywords: sec-moderate

Thanks for the review. I agree that, for the demonstrated end-to-end impact, the alternate endpoint must successfully authenticate as ORIGIN_HOST, negotiate the advertised HTTP/2 or HTTP/3 protocol, and accept requests for the origin authority.

However, I disagree with the statement that this necessarily requires “a certificate the attacker must control on a device in the victim’s private network.”

Firefox separates the authenticated origin from the routed endpoint. AltSvcMapping::GetConnectionInfo() constructs connection information using mOriginHost:mOriginPort as the authenticated origin and mAlternateHost:mAlternatePort as the network route. The socket layer resolves and connects to the alternate route, but passes mOriginHost to PSM for TLS security checks.

AltSvcTransaction::MaybeValidate() marks the mapping valid when the expected HTTP version has been negotiated and TLS verification has not failed. Firefox does not require the alternate endpoint to use the same certificate or private key as the original public endpoint, and it does not compare the alternate endpoint’s certificate with the certificate previously presented by the public endpoint.

The actual prerequisite is therefore:

The selected private endpoint must be independently capable of authenticating as ORIGIN_HOST, negotiating the advertised ALPN protocol, and accepting requests for the origin authority.

This is a real target-compatibility constraint, and I am not claiming that arbitrary private HTTPS endpoints are reachable. However, it is not equivalent to requiring the attacker to control a certificate or device inside the victim’s private network. The public and private endpoints may use different certificates and different private keys, provided that the private endpoint can successfully authenticate as ORIGIN_HOST.

As an illustrative example, an organization may operate separate public and internal frontends for the same hostname, with each frontend using its own certificate and private key valid for that hostname. If the public origin is compromised, the attacker can return an Alt-Svc header pointing to an attacker-controlled alternate hostname whose DNS resolves to the internal frontend’s private address. The internal frontend can then satisfy the TLS requirement even though the attacker does not control that endpoint, its certificate, or its private key.

This is only an example of a compatible deployment, but it demonstrates why successful authentication as ORIGIN_HOST is not equivalent to attacker control of a certificate inside the private network.

After validation, GetAltServiceMapping() returns the mapping because it is marked validated, and nsHttpChannel replaces the ordinary transaction’s connection information with connection information containing the private alternate route. In the quoted reuse path, no LNA authorization check occurs before that route is selected.

The runtime result demonstrates substantially more than a speculative connection or validation probe. Firefox sent cookie-bearing GET, POST, and navigation requests to the private address; JavaScript read the private endpoint’s response bodies; the private endpoint processed state-changing requests; and its navigation response was accepted as an ORIGIN_HOST document. The validation-only control received no request HEADERS, while an ordinary public-to-private fetch was blocked by LNA.

No unusual victim interaction is required: Alt-Svc validation, caching, and reuse are automatic, and no LNA permission decision or grant occurs.

I agree that the origin-identity requirement limits the set of compatible private targets. My disagreement is that the current explanation characterizes that requirement as an attacker foothold or attacker-controlled certificate inside the private network, which Firefox does not actually require.

Based on the demonstrated read/write impact and the actual prerequisites enforced by Firefox, I would appreciate human reconsideration of the rating as sec-high. If sec-moderate is retained because compatible private deployments are considered uncommon, could that be stated as the decisive factor instead of attacker control of a certificate or device inside the private network?

Flags: needinfo?(twsmith)

Thank you for the additional information. Waiting on Sunil to provide input.

Flags: needinfo?(twsmith)
Attached file (secure) —
Assignee: nobody → kershaw
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Pushed by kjang@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/a1b4e0775593 https://hg.mozilla.org/integration/autoland/rev/933732824d8e Don't apply the same-origin LNA exemption to Alt-Svc-rerouted connections, r=sunil,necko-reviewers
Group: network-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 3 months ago
Resolution: --- → FIXED
Target Milestone: --- → 154 Branch
Flags: needinfo?(smayya) → in-testsuite+
Keywords: regression
Regressed by: 2005990

The patch landed in nightly and beta is affected.
:kershaw, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(kershaw)

I think it's fine to let this ride the train.

Flags: needinfo?(kershaw)

That means this was part of the fix for CVE-2026-16380 and shouldn't get a separate advisory.

Whiteboard: [client-bounty-form][necko-triaged][necko-priority-queue] → [client-bounty-form][necko-triaged][necko-priority-queue][adv-main153-]

LNA is a new mechanism to protect against attacks that have been possible on the web for 25+ years. sec-moderate is not the right severity level

Flags: sec-bounty? → sec-bounty-
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: