I decided to use NSSecureCoding
over NSCoding
, but I'm having trouble getting it to work.
I would expect the following code to fail, since I'm encoding an NSString
but attempting to decode an NSNumber
. The object is initialized without throwing an exception, however.
+ (BOOL)supportsSecureCoding
{
return YES;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
// prints '1' as expected
NSLog(@"%d", coder.requiresSecureCoding);
// unexpectedly prints 'foo' (expecting crash)
NSLog(@"%@", [coder decodeObjectOfClass:NSNumber.class forKey:@"bar"]);
return [super init];
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:@"foo" forKey:@"bar"];
}
Here's the code I'm using to test the snippet from above:
MyClass *object = [[MyClass alloc] init];
NSMutableData *const data = [[NSMutableData alloc] init];
NSKeyedArchiver *const archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
archiver.requiresSecureCoding = YES;
[archiver encodeObject:object forKey:@"root"];
[archiver finishEncoding];
NSKeyedUnarchiver *const unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
unarchiver.requiresSecureCoding = YES;
[unarchiver decodeObjectOfClass:MyClass.class forKey:@"root"];
[unarchiver finishDecoding];
Am I missing something completely obvious or why is no exception thrown during decoding?
A protocol that enables encoding and decoding in a manner that is robust against object substitution attacks.
NSCoding is a protocol that you can implement on your data classes to support the encoding and decoding of your data into a data buffer, which can then persist on disk. Implementing NSCoding is actually ridiculously easy — that's why you may find it helpful to use.
The NSCoding protocol declares the two methods that a class must implement so that instances of that class can be encoded and decoded. This capability provides the basis for archiving (where objects and other structures are stored on disk) and distribution (where objects are copied to different address spaces).
NSCoder declares the interface used by concrete subclasses to transfer objects and other values between memory and some other format. This capability provides the basis for archiving (storing objects and data on disk) and distribution (copying objects and data items between different processes or threads).
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
torequiresSecureCoding
, then an exception will be thrown if the class to be decoded does not implementNSSecureCoding
or is notisKindOfClass:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With