Safeguard
Security Guides

Swift and iOS Security Best Practices: Storage, Transport, and the Dependency Supply Chain

iOS gives you a hardware-backed Keychain, Data Protection, and App Transport Security. Most iOS app breaches come from switching those defaults off — and from unaudited SwiftPM dependencies.

Daniel Osei
Security Researcher
6 min read

iOS is one of the most hardened consumer platforms in existence, and even it gets popped: CVE-2025-24085, a use-after-free in Apple's CoreMedia stack, was actively exploited against versions of iOS before 17.2 and landed in CISA's Known Exploited Vulnerabilities catalog in January 2025. But OS-level zero-days are Apple's problem to patch. The breaches that are your problem as an app developer are almost always self-inflicted: secrets written to UserDefaults, App Transport Security disabled to silence a warning, certificate validation stubbed out during development and shipped to production, and vulnerable third-party packages pulled in through Swift Package Manager. The platform hands you a hardware-backed Keychain, per-file Data Protection, and ATS for free. This guide is about using them, and about the dependency layer that undoes them if you ignore it.

Where should an iOS app store secrets?

In the Keychain, never in UserDefaults. UserDefaults is an unencrypted property list on disk that is captured in device backups and readable on a jailbroken device. The Keychain encrypts entries and lets you scope their accessibility to the device and the unlock state:

// SAFE -- Keychain, scoped to this device, only when unlocked
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "authToken",
    kSecValueData as String: token.data(using: .utf8)!,
    kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemAdd(query as CFDictionary, nil)

Use kSecAttrAccessibleWhenUnlockedThisDeviceOnly so the item never syncs to iCloud or restores onto another device. For files, apply Data Protection — set the file's protection class to .complete so its contents are encrypted whenever the device is locked. Never hardcode API keys or signing secrets in Swift source: they survive as plaintext strings in the shipped binary and are trivially extracted from an .ipa.

How do I keep transport security intact?

App Transport Security enforces TLS 1.2 or higher by default. The single most common iOS mistake is disabling it wholesale to make a plaintext endpoint work:

<!-- DANGEROUS -- turns off ATS for the entire app -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

Never ship NSAllowsArbitraryLoads. If you genuinely must reach one legacy host, scope an exception to that domain only. The parallel footgun lives in the networking code: overriding a URLSession delegate to accept every server trust turns HTTPS into an unauthenticated channel.

// DANGEROUS -- accepts any certificate, defeats TLS
func urlSession(_ s: URLSession, didReceive c: URLAuthenticationChallenge,
                completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    completionHandler(.useCredential, URLCredential(trust: c.protectionSpace.serverTrust!))
}

Validate the trust chain, and if you pin, pin against the public key or an intermediate you control rather than a leaf that rotates.

What about WKWebView and third-party dependencies?

A WKWebView that registers a script message handler and then loads untrusted content exposes native code to that page, the same bridge risk as on other platforms — only enable message handlers for content you control. The larger blind spot is the dependency graph. Swift Package Manager pins resolved versions in Package.resolved, and CocoaPods in Podfile.lock; commit both, pin to exact versions, and review new transitive packages before they ship. A single compromised networking or analytics SDK reaches every screen in your app, and framework discipline does nothing against a poisoned dependency.

iOS security checklist

PracticeWhy it matters
Store secrets in the Keychain, scoped ThisDeviceOnlyUserDefaults is plaintext and backed up
Apply .complete Data Protection to sensitive filesEncrypts file contents when the device is locked
Never ship NSAllowsArbitraryLoads; scope ATS exceptionsKeeps TLS 1.2+ enforced app-wide
Validate server trust; never accept all certificatesPrevents silent HTTPS downgrade
Restrict WKWebView message handlers to trusted contentBlocks the native-bridge injection path
Commit and pin Package.resolved / Podfile.lockLocks the dependency graph against swaps
Keep the deployment target patchedCloses exploited OS bugs like CVE-2025-24085

How Safeguard Helps

Safeguard's software composition analysis resolves your Swift Package Manager and CocoaPods graphs — the transitive SDKs you never chose directly included — and applies call-path reachability so a dependency CVE your code actually invokes is separated from one that never runs. When a patched release exists, auto-fix opens a tested pull request that moves you to the safe version, and developers can run the same analysis locally with the Safeguard CLI before a build is cut. If you are comparing tools, the Safeguard comparison hub breaks down where reachability-aware analysis differs from version-only scanners. Keychain discipline, ATS, and trust validation stay your responsibility; Safeguard secures the package layer beneath the app.

Bring your iOS supply chain under control — start for free or read the documentation.

Frequently Asked Questions

Is UserDefaults safe for storing a login token?

No. UserDefaults is an unencrypted property list that is included in device backups and readable on a jailbroken device. Authentication tokens, API keys, and any credential belong in the Keychain with an accessibility class such as kSecAttrAccessibleWhenUnlockedThisDeviceOnly, which keeps the item encrypted and bound to that single device.

When is it acceptable to disable App Transport Security?

Effectively never for the whole app. If a specific legacy host cannot support TLS 1.2, add a narrowly scoped exception for that one domain rather than setting NSAllowsArbitraryLoads, and treat it as technical debt with a deadline. A blanket ATS bypass removes transport protection from every request the app makes.

Do I need certificate pinning on iOS?

Pinning is valuable for high-risk apps such as banking and health, but it is optional and must be done carefully — pin to a public key or an intermediate you control, not a leaf certificate that rotates, or you will brick the app at renewal. What is not optional is validating the default trust chain: never override the URLSession delegate to accept every certificate.

How do I know if a third-party iOS SDK is vulnerable?

Track the versions pinned in Package.resolved and Podfile.lock and run them through a software composition analysis tool that maps known CVEs to those exact versions. Reachability analysis then tells you whether your app actually calls the vulnerable code path, so you fix the SDKs that matter rather than every advisory in the graph.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.