Mobile Development Bug Report

ITMS-91053 Missing API declaration: fixing an iOS privacy manifest rejection

A routine iOS release bounced from App Store Connect with ITMS-91053 for UserDefaults, file timestamps and system boot time. Our own code needed reasons, and an old statically linked SDK shipped no manifest at all. Here is the full cleanup.

Daniel Tran Daniel Tran @fun_dev · October 22, 2024 · 9 min read · 4 Views
Banner: App Store Connect ITMS-91053 Missing API declaration and an iOS privacy manifest

Our team maintains the iOS app for a chain of fitness studios: class booking, a QR check-in pass and workout history. Version 6.8 was a small release. Xcode uploaded the archive without complaint, then twenty minutes later App Store Connect emailed the build back as invalid:

ITMS-91053: Missing API declaration - Your app's code in the "StudioPass" file references one or more
APIs that require reasons, including the following API categories:
NSPrivacyAccessedAPICategoryUserDefaults.

ITMS-91053: Missing API declaration - Your app's code in the "LegacyMetrics" file references one or
more APIs that require reasons, including the following API categories:
NSPrivacyAccessedAPICategorySystemBootTime, NSPrivacyAccessedAPICategoryFileTimestamp.

Earlier in the year we had received the same messages as warnings and filed a ticket for later. Since May 1, 2024, Apple rejects uploads that use these APIs without a declared reason, and "later" had arrived.

Reproducing it

App Store Connect is the only authority here, but we needed a faster loop than uploading builds. Two local checks got us close:

  1. Xcode privacy report. In Organizer, right-click the archive and choose Generate Privacy Report. It merges every privacy manifest bundled in the app into one PDF. Ours listed nothing for LegacyMetrics and no API reasons for the app target.
  2. Symbol scan of the built binaries. Required-reason APIs show up as imported symbols:
APP="build/StudioPass.xcarchive/Products/Applications/StudioPass.app"
for bin in "$APP/StudioPass" "$APP"/Frameworks/*.framework/*; do
  [ -f "$bin" ] || continue
  hits=$(nm -u "$bin" 2>/dev/null | grep -E 'NSUserDefaults|systemUptime|mach_absolute_time|_stat$|_fstat$|NSFileModificationDate|volumeAvailableCapacity' | sort -u)
  [ -n "$hits" ] && printf '%s\n%s\n\n' "$bin" "$hits"
done

This is a heuristic, not Apple's exact scanner, but it pointed at the same two binaries.

What we ruled out

  • A missing manifest file in the app. We had a PrivacyInfo.xcprivacy since spring. It declared collected data types for the App Store privacy label but had an empty NSPrivacyAccessedAPITypes array.
  • Firebase and other large SDKs. Our up-to-date Firebase and Stripe pods ship their own manifests, which appeared correctly in the privacy report.
  • The manifest not being copied. We checked the built .app bundle; the file was present at the root with target membership set correctly.

Root cause

Two separate gaps. First, our own code used UserDefaults and FileManager attributes, and our manifest declared no reasons for them. Second, LegacyMetrics, a small analytics SDK from a vendor we had stopped paying, was pinned to a 2022 version. It called ProcessInfo.systemUptime and read file modification dates, and it shipped no privacy manifest. Apple treats an SDK's API usage as part of your app, so its missing declaration became our rejection.

What is happening under the hood

Apple designated a set of "required reason APIs" that can be used for device fingerprinting: reading file timestamps, system boot time, disk space, active keyboards and UserDefaults. Any binary in your app that uses them must declare, in a privacy manifest, one or more approved reason codes explaining why. Each code describes a narrow, allowed use.

Category Typical API Reason code we used Meaning
UserDefaults UserDefaults.standard CA92.1 Read and write data only accessible to the app itself
FileTimestamp attributesOfItem, stat C617.1 Timestamps of files inside the app container
SystemBootTime systemUptime, mach_absolute_time 35F9.1 Measure elapsed time between in-app events
DiskSpace volumeAvailableCapacityKey E174.1 Check space before writing files

The manifest is a property list named PrivacyInfo.xcprivacy. Each framework or resource bundle can carry its own, and Xcode aggregates them. Statically linked libraries have no bundle of their own, so their manifest must travel in a resource bundle that ends up in the app, or the vendor must ship an XCFramework that contains one.

Apple also publishes a list of commonly used third-party SDKs that must include a manifest and a signature. That list covers popular SDKs, but an obscure one like LegacyMetrics still counts for ITMS-91053 because the scanner looks at the code itself, not at the list.

Environment

Component Version / value
macOS 15.0.1
Xcode 16.0 (iOS 18.0 SDK)
Deployment target iOS 15.0
Dependency manager CocoaPods 1.15.2, use_frameworks! :linkage => :static for some pods
SDKs Firebase 11.3, Stripe 23.31, LegacyMetrics 2.4.1 (2022, no manifest)
Upload path Xcode Organizer to App Store Connect

Minimal reproduction

// Any of these in an app with no API reasons declared triggers ITMS-91053 on upload
let lastSync = UserDefaults.standard.object(forKey: "lastSync") as? Date
let attrs = try FileManager.default.attributesOfItem(atPath: cacheURL.path)
let modified = attrs[.modificationDate] as? Date
let uptime = ProcessInfo.processInfo.systemUptime

Who is affected

Every iOS, iPadOS, tvOS, visionOS and watchOS app uploaded to App Store Connect since May 1, 2024, including React Native and Flutter apps, because their engines and plugins use these APIs too. Cross-platform plugins pinned to old versions are a frequent source. TestFlight uploads are checked the same way.

The fix

1. Declare reasons for our own code. We added the accessed API types to the existing app manifest. Only reasons that match real behaviour; we checked each call site:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>NSPrivacyTracking</key>
  <false/>
  <key>NSPrivacyAccessedAPITypes</key>
  <array>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array><string>CA92.1</string></array>
    </dict>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array><string>C617.1</string></array>
    </dict>
  </array>
  <!-- NSPrivacyCollectedDataTypes unchanged, omitted here -->
</dict>
</plist>

CA92.1 fits because our defaults are app-private. If you share defaults through an App Group with a widget, that is a different code (1C8F.1); using the wrong one can fail App Review later even if the upload passes.

2. Replace the dead SDK. We could have written a manifest on the vendor's behalf, but we did not own its code and could not vouch for what it did with uptime and file dates. The two events it tracked already existed in Firebase Analytics, so we removed it:

# Podfile
- pod 'LegacyMetrics', '2.4.1'
pod deintegrate && pod install
grep -rn "LegacyMetrics" StudioPass/ --include=*.swift

Three call sites moved to our analytics wrapper.

3. Update the rest. pod outdated showed two more pods behind the versions that added manifests. We updated them, regenerated the privacy report, and confirmed each framework now listed its own accessed API types.

Alternatives and trade-offs

  • Add a manifest for a third-party static library yourself. Possible: put a PrivacyInfo.xcprivacy in a resource bundle that ships with the library. It is a stopgap. You are declaring reasons for code you did not write, and if the SDK is on Apple's commonly used list, it also needs the vendor's signature.
  • Remove the API calls. For systemUptime in our own code we could have used Date differences, but monotonic time is the right tool for measuring durations, so declaring 35F9.1 would have been more honest than a workaround. In the end only the removed SDK used it.
  • Pick a broad reason to make the error go away. Do not. Reasons are reviewed, and a declaration that does not match behaviour is a guideline violation.

How to test the fix

# Archive from the command line, the same configuration CI uses
xcodebuild -workspace StudioPass.xcworkspace -scheme StudioPass -configuration Release \
  -archivePath build/StudioPass.xcarchive archive

# Validate the manifest syntax
plutil -lint StudioPass/PrivacyInfo.xcprivacy

Then we generated the privacy report from Organizer again and checked that every binary flagged by the nm scan had a matching category. Finally we uploaded to TestFlight; App Store Connect processed the build with no ITMS-91053 email.

Confirming in production

  • Build 6.8 (412) processed in App Store Connect without warnings and passed App Review in a day.
  • The aggregated privacy report listed UserDefaults and FileTimestamp for the app target, and separate entries from the Firebase and Stripe manifests.
  • After release, crash-free sessions stayed at 99.8 percent, and analytics event volume for the two moved events matched the previous version within 2 percent, so removing the SDK lost no data.

Regression guard

  • A CI step runs the nm scan on every release archive and fails if it finds a required-reason symbol in a binary whose bundle has no PrivacyInfo.xcprivacy, unless the binary is on an allowlist we review.
  • Adding a pod requires a checklist item in the pull request template: does it ship a privacy manifest, and is it on Apple's commonly used SDK list?
  • We upload a TestFlight build at the start of each release cycle, not at the end, so App Store Connect findings arrive with time to act.

Security implications

Privacy manifests are about user privacy rather than code security, but the cleanup had a real benefit: an unmaintained analytics SDK with network access and file system reads is a supply chain risk. Removing it shrank our attack surface and dropped one outbound domain from the app. Declaring reasons honestly matters; a manifest that does not match the app's behaviour can lead to rejection or removal later.

Prevention checklist

  • Keep NSPrivacyAccessedAPITypes in your app manifest in sync with code that touches required-reason APIs.
  • Prefer SDK versions that ship their own privacy manifest, and remove abandoned SDKs.
  • Generate the Xcode privacy report for every release archive.
  • Treat App Store Connect warnings as deadlines, not information.
  • Upload early in the cycle to surface processing errors before release day.

References

Add a label

#ios #xcode #privacy-manifest #app-store-connect #cocoapods
Written by
Daniel Tran @fun_dev

I have spent most of my career on the side of software that pages people at night: payment integrations, web platforms with real traffic, CI/CD pipelines, Kubernetes clusters and the databases underneath them. Somewhere along the way I started keeping a notebook of every bug that cost more than an hour — the exact error, what we ruled out, the real root cause and how we proved the fix. Hibug is where those notes live. Every post follows the same path: Investigate, Explain, Fix, Verify — with the environment spelled out, the fix tested, and the security angle checked.

Advertisement

Comments

0

No comments yet. Be the first to comment!

Sign In to post a comment.