← All issues

[23] Pass an extra ApplicationBundleIdentifier through PCM

Severity: Low | Component: WebKit Networking / PCM | f8277ab

이번 commit은 vulnerability fix가 아닌 기능 추가에 해당하므로 Severity를 Low로 평가합니다. 다만 두 가지 구조적 변경이 포함되어 있어 주목이 필요합니다. privacy-proxy fail-closed flag가 loader의 무조건적 설정에서 중앙화된 helper의 조건부 설정으로 이동했고, adattributiond daemon의 sandbox와 entitlement가 직접 네트워크 egress를 허용하는 방향으로 확장되었습니다. 두 변경 모두 privacy invariant를 약화시키고 daemon의 attack surface를 넓히는 결과로 이어집니다.

"secondary" identifier(com.apple.webkit.adattributiond)가 PrivateClickMeasurementManagerNetworkLoader::start를 거쳐 전달되도록 추가되었습니다. adattributiond의 URLSession에는 해당 식별자가 NSURLSessionConfiguration을 통해 설정됩니다. 아울러 daemon에는 network-client/socket-delegate/networkserviceproxy entitlement와 com.apple.networkserviceproxy에 대한 mach-lookup 권한이 새로 부여되었습니다. _privacyProxyFailClosed flag는 PrivateClickMeasurementNetworkLoaderCocoa.mm의 무조건적 설정에서 NetworkDataTaskCocoa.mm::setPCMDataCarriedOnRequest 내부의 _needsNetworkTrackingPrevention 옆으로 이동했습니다. commit message에 따르면 이 flag는 이제 debug mode 외부에서만 적용됩니다.

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

WTF::switchOn(applicationBundleIdentifier,
- [&] (const String& bundleIdentifier) {
- configuration.get()._sourceApplicationBundleIdentifier = bundleIdentifier.createNSString().get();
+ [&] (const std::pair<String, String>& bundleIdentifiers) {
+ configuration.get()._sourceApplicationBundleIdentifier = bundleIdentifiers.first.createNSString().get();
+ configuration.get()._sourceApplicationSecondaryIdentifier = bundleIdentifiers.second.createNSString().get();
}, ...);
- [request _setPrivacyProxyFailClosed:YES];
+ [request setAttribution:NSURLRequestAttributionUser];

Source/WebKit/NetworkProcess/cocoa/NetworkDataTaskCocoa.mm

request._needsNetworkTrackingPrevention = YES;
+ request._privacyProxyFailClosed = YES;

Source/WebKit/Scripts/process-entitlements.sh

+ plistbuddy Add :com.apple.private.network.socket-delegate bool YES
+ plistbuddy Add :com.apple.security.network.client bool YES
+ plistbuddy Add :com.apple.private.networkserviceproxy bool YES
+ plistbuddy Add :com.apple.security.exception.mach-lookup.global-name:0 string com.apple.networkserviceproxy

Privacy-proxy fail-closed guard가 debug mode 조건부로 재배치되었으며, attribution daemon에 network egress 경로를 부여하는 sandbox/entitlement 완화와 함께 적용되었습니다.

ApplicationBundleIdentifierOrAuditTokenApplicationBundleIdentifiersOrAuditToken(Variant<std::pair<String, String>, Vector<uint8_t>>)으로 변경되었습니다. 이 타입은 PrivateClickMeasurementManager::create/ctor, NetworkLoader::start, managerOrProxy, initializePCMStorageInDirectory를 거쳐 전달됩니다. Cocoa loader는 _sourceApplicationBundleIdentifier_sourceApplicationSecondaryIdentifier 양쪽을 모두 설정합니다. PCMDaemonEntryPoint.mm에서는 secondary identifier가 com.apple.webkit.adattributiond로 하드코딩되었습니다. PCM report 요청에는 setAttribution:NSURLRequestAttributionUser가 설정되도록 변경되었으며, adattributiond sandbox profile은 com.apple.networkextension.uuidcache.plist를 읽을 수 있도록 완화되었습니다.

PCM은 WebKit의 privacy-preserving ad-click attribution 메커니즘입니다. source 사이트에서의 클릭 정보를 메타데이터로 저장하고, 이후 conversion이 발생하면 해당 source의 endpoint로 attribution report를 전송합니다. iOS에서는 PCM이 XPC daemon(adattributiond)으로 실행되며, NSURLSession을 통해 report를 전송합니다. _sourceApplicationBundleIdentifier_sourceApplicationSecondaryIdentifier는 발신 트래픽에 원본 애플리케이션의 identity를 태그하는 NSURLSessionConfiguration SPI입니다. _privacyProxyFailClosedNSMutableURLRequest SPI로, privacy relay를 사용할 수 없는 경우 직접 연결로 fallback하는 대신 요청 자체를 실패시키도록 networking stack에 지시합니다. 이 설정이 없으면 relay 장애 시 직접 연결로 저하되어 클라이언트 IP가 노출될 수 있습니다. networkserviceproxy는 sandboxed daemon의 네트워크 연결을 중개하는 시스템 서비스입니다.

이번 refactoring 자체는 기계적이며 ownership을 유지하는 변경입니다. Variant는 여전히 String 쌍 또는 Vector<uint8_t>를 값으로 보유하므로 lifetime 변화는 없습니다. 주목해야 할 것은 동작상의 두 가지 변화입니다. _privacyProxyFailClosed의 이동으로 인해 invariant가 "loader를 벗어나기 전 해당 request 객체에 설정"에서 "PCM 데이터 전달 여부를 조건으로 downstream 어딘가에서 설정"으로 범위가 좁아졌습니다. 향후 PCM-bound request를 생성하면서 setPCMDataCarriedOnRequest를 우회하는 code path가 추가될 경우, fail-closed 보장이 조용히 약화될 수 있으므로 주의가 필요합니다.

adattributiond daemon은 기존에 없던 network egress 권한을 새로 확보했습니다. 새로 열린 도달 가능한 상태는 다음과 같습니다. daemon이 client socket을 열고 networkserviceproxy와 통신할 수 있게 되었습니다. 가정되지만 강제되지 않는 invariant도 존재합니다. PCM report URL이 daemon이 내부적으로 구성하는 attestation/attribution endpoint에만 해당한다는 점(공격자가 제공한 URL 없음), secondary identifier만 사용된다는 점, 이동된 _privacyProxyFailClosed가 production build에서 모든 PCM-carried request에 적용된다는 점이 이에 해당합니다. 이러한 가정이 깨질 경우 새로 가능해지는 버그 유형은 다음과 같습니다. URL 구성이 upstream에서 공격자의 영향을 받는 경우 daemon에서 SSRF 또는 임의 호스트로의 egress가 발생할 가능성이 있습니다. 또한 어떤 code path에서든 _privacyProxyFailClosed가 누락되면 attribution endpoint에 IP 노출로 이어질 수 있습니다.

이번 변경은 PCM 주변의 network-egress confinement 모델을 약화시킵니다. "PCM 요청은 privacy proxy를 절대 우회하지 않는다"는 invariant가 "non-debug build에서, 조건부 경로가 실행된다는 가정 하에"로 범위가 좁아졌습니다. 다만 이번 commit 단독으로는 직접적인 exploitation primitive가 도입되지는 않습니다.