iOS IPC and XPC — source audit

iOS IPC and XPC — source audit

TL;DR: iOS apps talk to extensions, app groups, and (on macOS-Catalyst/SystemExtensions) XPC services. Source-audit risks: XPC interfaces with weak NSSecureCoding allow-lists (object substitution), setExportedInterface exposing too much, missing client audit-token validation, app-group shared containers used as untrusted inboxes, and Pasteboard as a “covert” IPC that anything on the device can sniff. Companion to ios-source-review-methodology and android-ipc-and-intent-source-audit.

Surfaces

Surface Where used
App Extensions (Share, Action, Today, Network) always-on, sandboxed
App Groups (shared Documents, UserDefaults) host ↔ extensions ↔ companion apps
XPC (NSXPCConnection) system / network / file-provider extensions, macOS-Catalyst
Custom URL schemes / Universal Links external IPC (ios-url-scheme-and-universal-link-audit)
Pasteboard any-to-any, but with iOS 14+ system warnings
Mach ports (low-level) rare in app code, common in SystemExtensions

XPC — the audit shape

1
2
3
4
5
6
7
8
let interface = NSXPCInterface(with: VaultServiceProtocol.self)
interface.setClasses(NSSet(objects: NSString.self, NSData.self) as! Set<AnyHashable>,
                     for: #selector(VaultServiceProtocol.store(_:value:reply:)),
                     argumentIndex: 1, ofReply: false)
let connection = NSXPCConnection(machServiceName: "com.example.vault", options: [])
connection.exportedInterface = interface
connection.exportedObject = VaultService()
connection.resume()

The four things to check:

1. Interface allow-lists (setClasses)

Without setClasses, decoded objects can be any NSSecureCoding class — including classes that aren’t yours, leading to “object substitution” decode-time gadgets. Always allow-list types per argument.

2. Validate the peer

1
2
3
4
5
6
connection.exportedInterface?.setClasses(...)
// On every invocation, verify the caller:
if let token = connection.auditToken {
    let peer = SecCodeCopyGuestWithAttributes([... audit token ...])
    // verify peer signing identity is one you trust
}

Greps:

1
grep -rn 'auditToken\|SecCodeCopyGuestWithAttributes\|SecRequirementCreateWithString' .

Absence on a privileged service is a finding.

3. Don’t trust the bundle identifier in a string

Calling code can lie about its identity via NSString fields. Identity comes from the audit token + code-signing requirement.

4. Interface surface

Look at the protocol. Every method is callable; each that touches sensitive state needs argument validation. Match Android’s onBind rules.

App Group shared containers

1
2
let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example.shared")
let inbox = url!.appendingPathComponent("inbox/")

Files in the shared container are written by any app in the group. Treat reads from this container as untrusted input.

Bugs:

  • Plist deserialisation of attacker-written file.
  • Image decode of attacker-written file (libpng/libjpeg CVEs).
  • “Restore my session from inbox/state.json” — config injection.

Shared UserDefaults

1
2
let defs = UserDefaults(suiteName: "group.com.example.shared")
defs?.set(token, forKey: "authToken")

A second app in the group with a different threat model can read/write. Audit the other app’s behaviour, not just yours.

Pasteboard

1
UIPasteboard.general.string = code   // BAD: any app can read

Patterns:

  • One-time codes copied for the user → another app on the device reads them.
  • Tokens or secrets written even briefly → visible to other apps via UIPasteboardChangedNotification.

Mitigations:

  • iOS 14+ warns when an app reads the pasteboard; users notice. Don’t read in viewDidAppear.
  • For sensitive flows use UIPasteboard with setItems(_:options: [.expirationDate: Date()...]).
  • Use named pasteboards with withName:create: for app-group-scoped sharing.

Extension boundary

Extensions are separate processes with their own entitlements. The host and extension talk through:

  • The extension principal class API (NSExtensionContext).
  • App-group shared containers.
  • Optional XPC.

Bugs cluster on:

  • Extension trusting NSExtensionItem.userInfo blindly.
  • Host treating extension-returned data as fully trusted.
1
grep -rn 'NSExtensionContext\|NSExtensionItem' .

Mach service names

1
grep -rn 'NSXPCConnection(machServiceName' .

A Mach service name not prefixed by your team ID can be hijacked. Use the team prefix and verify in Info.plist.

Distributed Notifications and Darwin Notifications

CFNotificationCenter Darwin notifications are system-wide. They carry no payload but can leak event timing. Avoid using for “user logged in” events.

Source-audit checklist

  • Every NSXPCInterface uses setClasses per argument.
  • Server validates client by audit token + code-signing requirement.
  • App-group shared container reads treated as untrusted.
  • Shared UserDefaults limited to non-sensitive data.
  • No secrets ever in UIPasteboard.general.
  • Mach service names team-prefixed and Info.plist-declared.
  • Extension boundaries enforce input validation both ways.

References