In August 2019, Apple shipped iOS 12.4 with fixes for two vulnerabilities that Google Project Zero researcher Natalie Silvanovich had found while probing iMessage's "interactionless" attack surface: CVE-2019-8646, an information leak that let an attacker read file-backed NSData off a target device, and CVE-2019-8647, a use-after-free in Core Data reachable through keyed unarchiving. Both bugs traced back to NSKeyedUnarchiver, the same Objective-C-era decoding machinery that Swift developers still reach for — often unknowingly, through NSCoding-conforming types layered underneath Codable models. Silvanovich presented the research at Black Hat USA 2019 under the title "Look, No Hands!," and the underlying lesson wasn't really about iMessage: it was that keyed archiving, by default, will instantiate any NSCoding-conforming class present in the process from data an attacker controls, with no type check until the object is already built. Seven years later, Swift's Codable protocol has changed the default posture for new code, but NSCoding is still load-bearing in enormous amounts of shipping iOS software — view controller state restoration, NSUserActivity payloads, Handoff, and any app carrying legacy Objective-C. This post walks through where the risk actually lives and what a safe pattern looks like.
What makes NSKeyedUnarchiver dangerous by default?
NSKeyedUnarchiver's legacy API — unarchiveObject(with:) and the plain decodeObject(forKey:) — performs no class validation during decoding. Given a serialized archive, the unarchiver reads the class name encoded in the data and instantiates whatever class by that name is loadable in the current process, then calls its init(coder:). If an attacker can influence the archive bytes (a file written to a shared container, a Handoff payload, a pasteboard item, a network response deserialized without validation), they can potentially trigger initializers on classes never intended to be built from untrusted input. This is the same category of bug as Java and .NET's well-documented insecure-deserialization issues (CWE-502): the vulnerability isn't in any single class, it's in the decoder trusting the input to tell it what to construct. Apple's own developer documentation flags the legacy unarchiving APIs as deprecated for exactly this reason and directs developers toward the secure variant.
How does NSSecureCoding actually fix this?
NSSecureCoding is a protocol extension of NSCoding that adds one required property: static var supportsSecureCoding: Bool. Adopting it doesn't change how you encode data — it changes how you're required to decode it. Instead of calling decodeObject(forKey:), which returns Any? and instantiates whatever class the archive claims, you call decodeObject(of:forKey:) (or the class-array variant, decodeObject(of:[AnyClass],forKey:)) and pass the class you actually expect. NSKeyedUnarchiver then verifies the decoded object's class matches the allow-list before returning it, rejecting anything else. This closes the exact gap Silvanovich's research exploited: the decoder is told in advance what it is allowed to build. Apple has required NSSecureCoding conformance for restorable state and Handoff payloads for years precisely because those are attacker-reachable paths on a device the user hasn't necessarily unlocked or interacted with — the "interactionless" class of bug the 2019 Black Hat talk was about.
Is Codable automatically safe?
Codable is structurally different from NSCoding in a way that removes an entire bug class: it's a value-oriented coder for structs and enums, driven by Decoder/KeyedDecodingContainer, and it never instantiates a class by name pulled from the payload — the concrete type is always known statically by the caller (try decoder.decode(User.self, from: data)). That eliminates the polymorphic type-confusion problem that made NSKeyedUnarchiver risky. But "safer by construction" isn't "safe by default." Custom init(from:) implementations that trust decoded values without bounds-checking, deeply nested or recursive JSON that a Decodable struct will happily recurse into (a resource-exhaustion vector on constrained mobile hardware), and unchecked integer or array-size fields are all still your responsibility. And any NSObject subclass that bridges Codable onto an underlying NSCoding/NSKeyedArchiver implementation for compatibility reinherits the old risk if that bridge isn't itself using NSSecureCoding.
What should a safe deserialization pattern look like in Swift?
The concrete pattern: for anything still using NSCoding, conform to NSSecureCoding, set supportsSecureCoding to true, and decode exclusively with decodeObject(of:forKey:) against a named class or an explicit allow-list — never the untyped legacy calls. Set NSKeyedUnarchiver.setClass(_:forClassName:) mappings explicitly for compatibility renames rather than letting the archiver resolve dynamically by whatever string is in the data. For Codable, prefer JSONDecoder/PropertyListDecoder over any custom NSCoding bridge, validate ranges and collection sizes inside init(from:) rather than trusting the decoded values verbatim, and treat any deserialization of data crossing a trust boundary — inter-process data, pasteboard, deep links, push notification payloads, cloud-synced records — as adversarial input requiring the same allow-list discipline as the NSSecureCoding case. Apple's App Sandbox and Data Protection reduce the blast radius of a successful exploit but do not substitute for validating the decode path itself.
This is one of the narrower corners of mobile appsec — most Swift codebases never touch raw NSKeyedUnarchiver calls directly — but it's exactly the kind of pattern that's easy to miss in code review because it looks like ordinary persistence code, not a security boundary. Static analysis that flags decodeObject(forKey:) and unarchived-object call sites, combined with confirming NSSecureCoding conformance on every type in the graph, is the practical way to catch it before it ships rather than after a researcher writes it up.