ATT framework requested before any tracking identifier is read
Why it matters
Apple's ATT framework (external apple-att-framework) requires explicit user authorisation before any app reads the IDFA or allows tracking SDKs to access cross-app identifiers. Initialising AdMob, Meta, or AppsFlyer in AppDelegate.application(_:didFinishLaunchingWithOptions:) before the ATT prompt resolves means those SDKs read IDFA before consent — a direct violation of Apple's policy, GDPR Art.7 (consent must precede processing), and CCPA §1798.120 (right to opt out of sale/sharing). Apple has rejected apps for this pattern since iOS 14.5 and continues to enforce it on every submission. Missing NSUserTrackingUsageDescription in Info.plist causes the app to crash when the ATT dialog is triggered.
Severity rationale
Critical because ATT violations cause immediate binary rejection, and reading IDFA before consent is grounds for enforcement action by Apple and regulators under GDPR Art.7.
Remediation
Request ATT authorisation before initialising any SDK that reads device identifiers. Trigger the prompt in applicationDidBecomeActive, not didFinishLaunchingWithOptions:
import AppTrackingTransparency
func applicationDidBecomeActive(_ application: UIApplication) {
ATTrackingManager.requestTrackingAuthorization { status in
DispatchQueue.main.async {
switch status {
case .authorized:
GADMobileAds.sharedInstance().start()
default:
GADMobileAds.sharedInstance().start() // limited mode
}
}
}
}
For Expo/React Native, use expo-tracking-transparency and await requestTrackingPermissionsAsync() before any SDK initialisation. Add NSUserTrackingUsageDescription to Info.plist with a specific description of the tracking purpose.
Detection
- ID:
att-before-tracking - Severity:
critical - What to look for: Count all relevant instances and enumerate each. Search for all locations where tracking identifiers or tracking-related APIs are accessed on iOS:
ASIdentifierManager.sharedManager().advertisingIdentifier(IDFA);SKAdNetworkattribution calls; third-party SDK initializations that read IDFA internally (AdMobGADMobileAds.sharedInstance().start(), MetaFBSDKApplicationDelegate.sharedInstance().application(...), AppsFlyerAppsFlyerLib.shared().start()). Then find whereATTrackingManager.requestTrackingAuthorization(completionHandler:)is called (orrequestPermissionsAsync()fromexpo-tracking-transparency). Verify the temporal ordering: ATT authorization must be requested and a result received BEFORE any tracking SDK is initialized or IDFA is read. Look for patterns where SDKs are initialized inAppDelegate.application(_:didFinishLaunchingWithOptions:)or a top-leveluseEffectBEFORE the ATT prompt appears. Also check: isNSUserTrackingUsageDescriptionset inInfo.plist? (Required — without it, the ATT dialog crashes the app on iOS 14+.) Does the app handle the.deniedand.restrictedcases by switching to a limited-data or contextual-ads-only mode? - Pass criteria: ATT authorization is requested before any tracking SDK reads a device identifier. At least 1 implementation must be verified. The app handles all four ATT status cases (authorized, denied, restricted, notDetermined) gracefully.
NSUserTrackingUsageDescriptionis set with a specific explanation. - Fail criteria: Tracking SDKs initialized before ATT authorization is received; IDFA accessed without an ATT prompt;
NSUserTrackingUsageDescriptionmissing; ATT denial not handled (app crashes or breaks tracking-dependent features). - Skip (N/A) when: App has no advertising or tracking SDKs and does not read IDFA or any cross-app tracking identifier. (Note: apps that only use analytics for first-party measurement without cross-app tracking may not need ATT — but confirm no IDFA is read.)
- Detail on fail:
"GADMobileAds.sharedInstance().start() called in AppDelegate before ATTrackingManager.requestTrackingAuthorization — AdMob reads IDFA before consent is collected"or"NSUserTrackingUsageDescription missing from Info.plist — app will crash when ATT dialog is triggered on iOS 14+" - Remediation: Violating ATT requirements is an immediate rejection and a potential privacy violation enforcement action from Apple.
- Request ATT before any SDK initialization that reads IDFA:
// AppDelegate or app startup import AppTrackingTransparency func applicationDidBecomeActive(_ application: UIApplication) { ATTrackingManager.requestTrackingAuthorization { status in DispatchQueue.main.async { switch status { case .authorized: // Initialize tracking SDKs GADMobileAds.sharedInstance().start() default: // Initialize SDKs in limited/contextual mode GADMobileAds.sharedInstance().start() // Ensure IDFA is not passed } } } } - For Expo/React Native, use
expo-tracking-transparency:import { requestTrackingPermissionsAsync } from 'expo-tracking-transparency'; const { status } = await requestTrackingPermissionsAsync(); // Initialize SDKs after this resolves - Request ATT in
applicationDidBecomeActive(notdidFinishLaunchingWithOptions) to ensure the dialog appears at the right time
- Request ATT before any SDK initialization that reads IDFA:
External references
- external · apple-att-framework — Apple App Tracking Transparency (ATTrackingManager)
- cwe · CWE-359 — Exposure of Private Personal Information to an Unauthorized Actor
- gdpr · Art.7 — Conditions for consent
- ccpa · §1798.120 — Right to opt-out of sale of personal information
Taxons
History
- 2026-04-18·v1.0.0·Initial import from app-store-privacy-data·automated