In this blog post I tell you how to test object substitution attacks and how using NSSecureCoding
can prevent such attacks. I will also
- share example code on how to use
NSKeyedArchiver
andNSKeyedUnArchiver
either with or withoutNSSecureCoding
. - explain how to archive / unarchive an object into different formats (binary vs. XML).
If you are not too familiar with the idea behind NSSecureCoding
when using NSKeyedArchiver
/ NSKeyedUnarchiver
then I recommend that you watch Apple's WWDC 2018 session Data You Can Trust.
Additionally I recommend to read the following articles as those helped me to deepen my understanding.
Object substitution attack
First let's encode data with NSKeyedArchiver
in the form of XML.
let archiver = NSKeyedArchiver(requiringSecureCoding: false)
archiver.outputFormat = .xml
archiver.encodeRootObject(yourObject)
archiver.finishEncoding()
let data = archiver.encodedData
archive.write(to: localURL)
The serialization result for my custom Model
class
class Model: NSObject, NSCoding {
var text: String
var amount: Double
// further code is omitted for readability
}
is the following in XML:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>$archiver</key>
<string>NSKeyedArchiver</string>
<key>$objects</key>
<array>
<string>$null</string>
<dict>
<key>$class</key>
<dict>
<key>CF$UID</key>
<integer>3</integer>
</dict>
<key>amount</key>
<real>22.219999999999999</real>
<key>text</key>
<dict>
<key>CF$UID</key>
<integer>2</integer>
</dict>
</dict>
<string>Test</string>
<dict>
<key>$classes</key>
<array>
<string>NSSecureCodingExample.Model</string>
<string>NSObject</string>
</array>
<key>$classname</key>
<string>NSSecureCodingExample.Model</string>
</dict>
</array>
<key>$top</key>
<dict>
<key>$0</key>
<dict>
<key>CF$UID</key>
<integer>1</integer>
</dict>
</dict>
<key>$version</key>
<integer>100000</integer>
</dict>
</plist>
As an attacker I can manipulate the information in this document in such a way that not Model
gets loaded during deserialization but another class.
For example I can change NSSecureCodingExample.Model
to NSSecureCodingExample.FakeModel
which corresponds to a class in my app
import Foundation
// For Testing Purposes only
class FakeModel: NSObject, NSCoding {
let text: String
let amount: Double
internal init(text: String, amount: Double) {
self.text = text
self.amount = amount
}
// ...
required init?(coder: NSCoder) {
/**
If serialized object in XML format was modified to use FakeModel
<dict>
<key>$classes</key>
<array>
<string>NSSecureCodingExample.FakeModel</string>
<string>NSObject</string>
</array>
<key>$classname</key>
<string>NSSecureCodingExample.FakeModel</string>
</dict>
Then app would crash :--!!!
*/
assert(1 == 2)
}
}
I can unarchive the object with NSKeyedUnarchiver
later.
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data) // This initializer enables requiresSecureCoding by default
unarchiver.requiresSecureCoding = false
let decodedDataObject = try unarchiver.decodeTopLevelObject()
unarchiver.finishDecoding()
// then cast decodedDataObject to your expected structured type
The app will crash because FakeModel
gets loaded during the decodeTopLevelObject
execution and it the FakeModel
initializer contains malicious code (an assertion).
Apple introduced the NSSecureCoding
protocol and multiple API enhancements for NSKeyedArchiver
and NSKeyedUnarchiver
. Once you adopt those then the compiler will be able to perform gated class checks so that an incorrect initialization cannot occur.
You can find the complete code, incl. a SwiftUI test application, on GitHub.
I made an interesting finding as it appears that it is not possible to unarchive an object stored in XML format which was archived with Secure Coding. If I am mistaken then I'd love to hear from you :)