What is NSSecureCoding, what attack does it prevent, and how do you unarchive data from an untrusted source?

14.8.1 · L3 · Interview

Predict first

Does a secure unarchive run the attacker’s initialiser?

import Foundation

class Evil: NSObject, NSCoding {
    override init() { super.init() }
    required init?(coder: NSCoder) { print("Evil.init(coder:) ran"); super.init() }
    func encode(with coder: NSCoder) {}
}
class Note: NSObject, NSSecureCoding {
    static var supportsSecureCoding = true
    var text: String
    init(_ t: String) { text = t }
    required init?(coder: NSCoder) { text = coder.decodeObject(of: NSString.self, forKey: "t") as String? ?? ""; super.init() }
    func encode(with coder: NSCoder) { coder.encode(text, forKey: "t") }
}
let evilData = try NSKeyedArchiver.archivedData(withRootObject: Evil(), requiringSecureCoding: false)
do { _ = try NSKeyedUnarchiver.unarchivedObject(ofClass: Note.self, from: evilData) }
catch { print("secure unarchive as Note fails:", (error as NSError).domain, (error as NSError).code) }
let loose = try NSKeyedUnarchiver(forReadingFrom: evilData)
loose.requiresSecureCoding = false
_ = loose.decodeObject(forKey: NSKeyedArchiveRootObjectKey)
print("insecure unarchiver ran the attacker's class")
let good = try NSKeyedArchiver.archivedData(withRootObject: Note("hello"), requiringSecureCoding: true)
print("secure round trip:", try NSKeyedUnarchiver.unarchivedObject(ofClass: Note.self, from: good)!.text)
do { _ = try NSKeyedArchiver.archivedData(withRootObject: Evil(), requiringSecureCoding: true) }
catch { print("archiving a non-secure class with requiringSecureCoding:", (error as NSError).code) }
  • secure unarchive as Note fails: NSCocoaErrorDomain 4864 / Evil.init(coder:) ran / insecure unarchiver ran the attacker's class / secure round trip: hello / archiving a non-secure class with requiringSecureCoding: 4866
  • Evil.init(coder:) ran / secure unarchive as Note fails: NSCocoaErrorDomain 4864
  • secure round trip: hello / insecure unarchiver ran the attacker's class

Mental model. Insecure unarchiving is a delivery driver told to bring in whatever the label says. Secure coding gives the driver a guest list: anything not on it stays outside.

Unarchiving builds objects of classes named in the data. If the decoder lets the data choose any class, a crafted archive can instantiate and run initialisers of classes you never intended (init(coder:) of an attacker-chosen class), which has been the start of real exploits. Secure coding makes the caller state the expected classes: NSKeyedUnarchiver.unarchivedObject(ofClass:from:) and decodeObject(of:forKey:) accept only the listed types, and every class involved must opt in with supportsSecureCoding = true. In the check an archive of an attacker class is rejected when decoded as Note (NSCocoaErrorDomain 4864, “value not found”-style failure) and its initialiser does not run, while a loose unarchiver with requiresSecureCoding = false does run the attacker’s init(coder:). Archiving with requiringSecureCoding: true a class that does not support it fails with 4866. The legacy NSKeyedUnarchiver.unarchiveObject(with:) and unarchiveTopLevelObjectWithData paths are the unsafe ones to avoid.

Apply it wherever bytes come from outside your control: pasteboard data, files in shared containers or Documents that other apps or iTunes file sharing can write, data from an extension, push payloads and anything restored from a backup you do not fully trust. Prefer Codable with JSONDecoder or PropertyListDecoder for your own models: they decode only the concrete types you name, so the class-injection problem does not exist, and they fail with typed errors you can handle. For NSKeyedArchiver payloads you must keep (state restoration, UIPasteboard, older stores), decode with an explicit class set, validate the decoded values (ranges, lengths, allowed strings), and cap sizes. Not run: the full set of sandbox escapes the old API enabled, which depends on available gadget classes.

The initialiser only runs on the insecure path; the secure path rejects the unexpected class.

Common traps

  • Using the old unarchiveObject(with:) on data you did not create. The data chooses the classes.
  • Forgetting supportsSecureCoding on a custom class. Decoding then fails, and the temptation is to turn security off.
  • Trusting values after a successful decode. Validate ranges and lengths yourself.

They might follow up with

  • Why is Codable safer? The concrete type is fixed in your code, not named by the input.
  • Where do untrusted archives come from in an app? Pasteboard, shared containers, file import and restored state.

Related questions

  • 14.8.2 Where else besides SQL can injection happen in an iOS app: NSPredicate, JavaScript in a web view, URLs, and…
  • 14.4.1 Why are deep links an attack surface, and how do you validate them?
  • 14.3.3 How do you keep personal data out of logs, screenshots, the pasteboard and the app switcher?

Where else besides SQL can injection happen in an iOS app… →