← All issues

[3] Connections for Private Click Measurement are not proxied

Severity: High | Component: WebKit Private Click Measurement (Network Process) | 975a11f

Rated High because the observable effect is complete user IP address disclosure on every PCM attribution report and token fetch — a direct bypass of the privacy proxy that PCM architecturally depends on — with confidence 0.93 based on the missing _sourceApplicationBundleIdentifier and cross-site classification shown in the diff, though the exact proxy-routing behavior of Apple's network stack is inferred from domain knowledge not fully verifiable from the diff alone.

PCM attribution reports and fraud prevention token fetches were not being routed through the privacy proxy because they lacked two signals required by Apple's network stack: (1) association with the application bundle (e.g., Safari), and (2) classification as third-party resource requests. The fix plumbs through the application bundle identifier or audit token and fabricates a cross-site mainDocumentURL. As a safeguard, the patch also sets _setPrivacyProxyFailClosed:YES so requests fail rather than silently falling back to direct connections when the proxy is unavailable.

Source/WebKit/NetworkProcess/PrivateClickMeasurement/cocoa/PrivateClickMeasurementNetworkLoaderCocoa.mm

- static NSURLSession *statelessSessionWithoutRedirectsSingleton()
+ static NSURLSession *statelessSessionWithoutRedirectsSingleton(const ApplicationBundleIdentifierOrAuditToken& applicationBundleIdentifier)
{
...
configuration.get()._shouldSkipPreferredClientCertificateLookup = YES;
+
+ WTF::switchOn(applicationBundleIdentifier,
+ [&] (const String& bundleIdentifier) {
+ configuration.get()._sourceApplicationBundleIdentifier = bundleIdentifier.createNSString().get();
+ }, [&] (const Vector<uint8_t>& auditToken) {
+ configuration.get()._sourceApplicationAuditTokenData = [NSData dataWithBytes:auditToken.span().data() length:auditToken.size()];
+ }
+ );
+
...
auto request = adoptNS([[NSMutableURLRequest alloc] initWithURL:url.createNSURL().get()]);
+ [request _setPrivacyProxyFailClosed:YES];
[request setValue:WebCore::HTTPHeaderValues::maxAge0().createNSString().get() forHTTPHeaderField:@"Cache-Control"];
[request setValue:WebCore::standardUserAgentWithApplicationName({ }).createNSString().get() forHTTPHeaderField:@"User-Agent"];
+ RetainPtr crossSiteMainDocument = [NSURLComponents componentsWithURL:request.get().URL resolvingAgainstBaseURL:NO];
+ crossSiteMainDocument.get().host = [NSString stringWithFormat:@"not-%@", crossSiteMainDocument.get().host];
+ [request setMainDocumentURL:crossSiteMainDocument.get().URL];
- RetainPtr task = [statelessSessionWithoutRedirectsSingleton() dataTaskWithRequest:request.get() ...];
+ RetainPtr task = [statelessSessionWithoutRedirectsSingleton(applicationBundleIdentifier) dataTaskWithRequest:request.get() ...];

The patch threads an ApplicationBundleIdentifierOrAuditToken (a Variant<String, Vector<uint8_t>>) from NetworkSession through PrivateClickMeasurementManager into every PCM::NetworkLoader::start() call. On the Cocoa side, statelessSessionWithoutRedirectsSingleton() now configures the NSURLSessionConfiguration with either _sourceApplicationBundleIdentifier or _sourceApplicationAuditTokenData depending on which is available. The request itself sets _setPrivacyProxyFailClosed:YES and fabricates a cross-site mainDocumentURL by prepending not- to the request host, forcing the URL loading system to classify the request as a third-party subresource. The daemon entry point (PCMDaemonEntryPoint.mm) hardcodes the Safari bundle identifier for the daemon code path.

Missing application identity and cross-site classification on privacy-sensitive network requests, causing proxy bypass.

Private Click Measurement (PCM) is WebKit's privacy-preserving ad attribution system. When a user clicks an ad on a source site and later converts on a destination site, PCM sends an attribution report to the destination. To prevent tracking, these reports are designed to be sent through a privacy proxy that hides the user's IP address. PCM also fetches fraud prevention token key material from servers, which should similarly be proxied.

Apple's network stack uses two signals to decide whether to proxy a connection: the source application identity (_sourceApplicationBundleIdentifier or _sourceApplicationAuditTokenData on NSURLSessionConfiguration), which associates the request with a known app like Safari, and whether the request is a third-party (cross-site) subresource, determined by comparing the request URL's host to the mainDocumentURL's host. Both must be present for proxy routing to engage. The _setPrivacyProxyFailClosed:YES directive on NSMutableURLRequest ensures that if the proxy is unreachable, the request fails rather than falling back to a direct connection.

Before the fix, PCM network requests were sent without either of the two signals needed for proxy routing. The NSURLSessionConfiguration lacked a source application identifier, and the requests had no mainDocumentURL to trigger cross-site classification. Without both signals, Apple's network proxy infrastructure (if the domain-knowledge understanding of its routing logic is correct) treated these as ordinary first-party requests and sent them directly. There was also no fail-closed behavior — if the proxy were unavailable, requests would silently degrade to direct connections rather than failing.

The fix is architecturally interesting in how it forces cross-site classification: it fabricates a mainDocumentURL by prepending not- to the request's own host (not-example.com for a request to example.com), guaranteeing a host mismatch that triggers third-party classification. This is a pragmatic workaround — PCM requests have no natural "first-party" document context since they originate from the Network Process, not from a page load.

🔒

Explores the privacy model implications and the specific network-level signals that were missing, along with the fail-closed defense-in-depth measure

Subscribe to read more

🔒

Multiple audit patterns identified for proxy bypass variants across WebKit's network subsystems, with concrete search targets

Subscribe to read more