Enforcing types with NSSecureCoding Enforcing types with NSSecureCoding objective-c objective-c

Enforcing types with NSSecureCoding


Looking at the definition of -[NSCoder decodeObjectOfClass:forKey:], yes, your code sample should have thrown an exception. The method's description says it:

Decodes an object for the key, restricted to the specified class.

And the discussion says:

If the coder responds YES to requiresSecureCoding, then an exception will be thrown if the class to be decoded does not implement NSSecureCoding or is not isKindOfClass: of aClass.

There are two inconsistencies with NSKeyedUnarchiver's implementation of this method, related to optimizations it does. The first is that decodeObjectOfClass:forKey: and decodeObjectForKey: only decode an object the first time it is encountered.

For example, the assert in the following code passes because foo and foo2 started out as the same object and were decoded just once while foo3 started as a separate object and as a result decoded separately.

func encodeWithCoder(coder:NSCoder) {    let foo = NSSet(objects: 1, 2, 3)    coder.encodeObject(foo, forKey: "foo")    coder.encodeObject(foo, forKey: "foo2")    coder.encodeObject(NSSet(objects: 1, 2, 3), forKey: "foo3")}required init(coder: NSCoder) {    let foo = coder.decodeObjectOfClass(NSSet.self, forKey: "foo")    let foo2 = coder.decodeObjectOfClass(NSSet.self, forKey: "foo2")    let foo3 = coder.decodeObjectOfClass(NSSet.self, forKey: "foo3")    assert(foo === foo2)    assert(foo !== foo3)    super.init()}

It appears that classes are only checked when the object is actually being decoded. The list of approved classes is compared against the class the object is requesting. So in my previous example, I could change the class for foo2 to be whatever I want and the code will still run and return an NSSet:

required init(coder: NSCoder) {    let foo = coder.decodeObjectOfClass(NSSet.self, forKey: "foo")    let foo2 = coder.decodeObjectOfClass(NSMutableDictionary.self, forKey: "foo2")    assert(foo === foo2)    super.init()}

The second inconsistency, related directly to your example, is that certain object types are never actually decoded. NSKeyedArchiver stores all its data as a binary property list, which according to Apple's source code has native support for string, data, number, date, dictionary, and array types. When NSKeyedArchiver encounters an NSString, NSNumber, or NSData object (but not a subclass), instead of encoding it using encodeWithObject: and saving information about how to decode it, it just stores the value directly in the PList. Then when you call decodeObjectOfClass:withKey: it sees the already present string and returns it right away without decoding. No decoding means no class check.

Whether this behavior is good or bad could be debated. Less checking means faster code but the behavior really doesn't match the API documentation. That said, you may be wondering what secure coding gets you if it doesn't guarantee return types. What secure coding with NSKeyedUnarchiver protects you from is that a maliciously crafted archive can't get you to call alloc/initWithCoder: on an arbitrary class. If you want more than that you could create a subclass that validates the output type of all decodeObjectOfClass:withKey: and decodeObjectOfClasses:withKey: call.