Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Enforcing types with NSSecureCoding

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?

like image 463
Christian Schnorr Avatar asked Mar 24 '15 21:03

Christian Schnorr


People also ask

What is Nssecurecoding?

A protocol that enables encoding and decoding in a manner that is robust against object substitution attacks.

What is NSCoding in iOS?

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.

What is NSCoding protocol?

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).

What is NSCoder in Swift?

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).


1 Answers

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.

like image 199
Brian Nickel Avatar answered Sep 27 '22 21:09

Brian Nickel