CVE-2025-43376
Overview
Background
- iCloud Private Relay
- Apple privacy feature that routes a user’s web traffic (and is expected to route DNS) through two relays so the local network and ISP cannot see the destinations being contacted.
- nw_resolver
- A CFNetwork/Network.framework object that performs hostname resolution according to given parameters; it can be created from an endpoint or from an already-evaluated network path.
- nw_path_evaluator / nw_path
- Network.framework machinery that evaluates how traffic to an endpoint should egress (interface, proxy, relay), producing an nw_path that encodes that routing decision.
- DNS prefetch
- A browser optimization (e.g. rel=dns-prefetch) that resolves hostnames ahead of use, meaning hostnames can be looked up automatically from page content.
- privacy_level_silent
- An nw_context privacy setting used here to suppress metadata/logging on the resolution, orthogonal to whether the query is actually routed through Private Relay.
Root Cause Analysis
DNSResolveQueueCFNet::performDNSLookup (Source/WebCore/platform/network/cf/DNSResolveQueueCFNet.cpp) performs WebCore’s asynchronous DNS prefetch/resolution using CFNetwork’s low-level nw_resolver API. Before the patch it configured the resolver’s parameters and privacy context (nw_context_set_privacy_level(…, nw_context_privacy_level_silent) and nw_parameters_set_context) and then created the resolver directly from the target endpoint via nw_resolver_create_with_endpoint(hostEndpoint, parameters). The invariant that must hold when iCloud Private Relay is enabled is that name resolution — like the connection itself — must be carried over the Private Relay proxy path rather than being sent as a plaintext query to the locally configured/default DNS servers. Creating a resolver directly from an endpoint apparently does not run the network path evaluation that decides how (and through which proxy/interface) traffic should egress, so the DNS query could be issued outside the Private Relay tunnel, exposing the hostnames being resolved to the local network / ISP even though Private Relay was on.
The fix inserts an explicit path-evaluation step: it creates an nw_path_evaluator for the endpoint with the same parameters (nw_path_create_evaluator_for_endpoint), copies the resulting nw_path (nw_path_evaluator_copy_path), and then builds the resolver from that path (nw_resolver_create_with_path(path)). Resolving over the evaluated path makes the query honor the Private Relay routing/proxy decision, restoring the invariant that DNS is tunneled with the rest of the connection. The companion change in Source/WebCore/PAL/pal/spi/cf/CFNetworkSPI.h merely declares the newly used SPI (the nw_path_evaluator object type and the three functions), which is a prerequisite for calling them and not itself a bug. That the pre-patch path leaked queries is grounded in the CVE description and the direction of the fix; the precise internal reason nw_resolver_create_with_endpoint skips proxy routing is an inference about CFNetwork behavior, not something the diff itself spells out.
Attack Path
- Victim relies on Private Relay for privacy A user browses with iCloud Private Relay enabled, expecting DNS as well as connection traffic to be hidden from the local network and ISP.
- Page triggers DNS resolution Any navigation or resource with hostnames — including link rel=dns-prefetch or many subresources on an attacker-controlled page — causes WebCore to call DNSResolveQueueCFNet::performDNSLookup for those hosts.
- Resolver created directly from endpoint Pre-patch, the resolver is built via nw_resolver_create_with_endpoint without path evaluation, so the query is emitted over the default network path instead of the Private Relay proxy.
- On-path observer captures queries A local-network or ISP-level observer (the attacker) sees the plaintext DNS queries, learning which hostnames the victim is visiting despite Private Relay being on.
- Deanonymization / tracking The leaked query stream lets the observer profile the victim’s browsing, defeating the privacy guarantee; there is no memory corruption and no code execution involved.
Impact Assessment
Files Changed
Source/WebCore/PAL/pal/spi/cf/CFNetworkSPI.hSource/WebCore/platform/network/cf/DNSResolveQueueCFNet.cpp
Audit Directions
- Other resolver/connection creations in the CFNet DNS pathWithin DNSResolveQueueCFNet.cpp and sibling CFNet networking code, grep for ’nw_resolver_create_with_endpoint’, ’nw_connection_create’, and any endpoint-first object creation that skips ’nw_path_create_evaluator’ — verify each privacy-sensitive lookup goes through an evaluated path when Private Relay/advanced privacy is active.
- Private Relay routing consistency across platformsCompare the CFNet resolver flow with other DNSResolveQueue backends and platform DNS/proxy code; grep for ‘privacy_level’, ‘PrivateRelay’, ‘HAVE(SYSTEM_SUPPORT_FOR_ADVANCED_PRIVACY_PROTECTIONS)’, and ’nw_parameters’ to find places that set a privacy context but never evaluate a proxy path.
- New SPI usage correctnessAudit every caller of nw_resolver_create_with_path / nw_path_evaluator_copy_path (grep those symbols) to ensure the evaluated path is non-null and actually reflects the intended endpoint+parameters, so a failed evaluation does not silently fall back to a leaking default resolution.
Patch
diff --git a/Source/WebCore/PAL/pal/spi/cf/CFNetworkSPI.h b/Source/WebCore/PAL/pal/spi/cf/CFNetworkSPI.h
index 3cc0d9be7a7f..54dd24f0dd90 100644
--- a/Source/WebCore/PAL/pal/spi/cf/CFNetworkSPI.h
+++ b/Source/WebCore/PAL/pal/spi/cf/CFNetworkSPI.h
@@ -97,6 +97,7 @@ OS_OBJECT_DECL(nw_context);
OS_OBJECT_DECL(nw_endpoint);
OS_OBJECT_DECL(nw_resolver);
OS_OBJECT_DECL(nw_parameters);
+OS_OBJECT_DECL(nw_path_evaluator);
OS_OBJECT_DECL(nw_proxy_config);
OS_OBJECT_DECL(nw_protocol_options);
OS_OBJECT_DECL(nw_establishment_report);
@@ -119,6 +120,8 @@ struct nw_protocol_options;
typedef struct nw_protocol_options *nw_protocol_options_t;
struct nw_establishment_report;
typedef struct nw_establishment_report *nw_establishment_report_t;
+struct nw_path_evaluator;
+typedef struct nw_path_evaluator *nw_path_evaluator_t;
#endif // OS_OBJECT_USE_OBJC
#if HAVE(NW_PROXY_CONFIG) || HAVE(SYSTEM_SUPPORT_FOR_ADVANCED_PRIVACY_PROTECTIONS)
@@ -148,6 +151,9 @@ bool nw_resolver_set_update_handler(nw_resolver_t, dispatch_queue_t, nw_resolver
bool nw_resolver_cancel(nw_resolver_t);
void nw_context_set_privacy_level(nw_context_t, nw_context_privacy_level_t);
void nw_parameters_set_context(nw_parameters_t, nw_context_t);
+OS_OBJECT_RETURNS_RETAINED nw_path_evaluator_t nw_path_create_evaluator_for_endpoint(nw_endpoint_t, nw_parameters_t);
+OS_OBJECT_RETURNS_RETAINED nw_path_t nw_path_evaluator_copy_path(nw_path_evaluator_t);
+OS_OBJECT_RETURNS_RETAINED nw_resolver_t nw_resolver_create_with_path(nw_path_t);
OS_OBJECT_RETURNS_RETAINED nw_endpoint_t nw_establishment_report_copy_proxy_endpoint(nw_establishment_report_t);
OS_OBJECT_RETURNS_RETAINED nw_context_t nw_context_create(const char *);
diff --git a/Source/WebCore/platform/network/cf/DNSResolveQueueCFNet.cpp b/Source/WebCore/platform/network/cf/DNSResolveQueueCFNet.cpp
index d9f720b492d4..9d9249ed7aba 100644
--- a/Source/WebCore/platform/network/cf/DNSResolveQueueCFNet.cpp
+++ b/Source/WebCore/platform/network/cf/DNSResolveQueueCFNet.cpp
@@ -125,7 +125,9 @@ void DNSResolveQueueCFNet::performDNSLookup(const String& hostname, Ref<Completi
nw_context_set_privacy_level(context.get(), nw_context_privacy_level_silent);
nw_parameters_set_context(parameters.get(), context.get());
- RetainPtr resolver = adoptCF(nw_resolver_create_with_endpoint(hostEndpoint.get(), parameters.get()));
+ RetainPtr pathEvaluator = adoptCF(nw_path_create_evaluator_for_endpoint(hostEndpoint.get(), parameters.get()));
+ RetainPtr path = adoptCF(nw_path_evaluator_copy_path(pathEvaluator.get()));
+ RetainPtr resolver = adoptCF(nw_resolver_create_with_path(path.get()));
RELEASE_ASSERT_WITH_MESSAGE(isMainThread(), "Always create timer on the main thread.");
auto timeoutTimer = makeUnique<Timer>([resolver, completionHandler]() mutable {