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.
[super initWithCoder:]
instead of[super init]
? – RobotizeNSObject
doesn't implementNSCoding
afaik. I assume the call would crash my app. But I'll give it a try. Edit: calling[super initWithCoder:]
won't even let my app compile. Besides, initialization shouldn't even matter in this context - I just want to retrieve a value from the coder. – Acyclic[@"foo" mutableCopy]
or@[]
you trigger a crash. Similarly@100
expectingNSString.class
doesn't. I will write up a full answer tomorrow but I suspect this has to do with how low level types are stored in the backing plist. (Strings are a plist data type and have no security risk for decoding.) – Introversion