DEV Community

Cover image for Swift deserialization security primer
SnykSec for Snyk

Posted on • Originally published at snyk.io

Swift deserialization security primer

Deserialization is the process of converting data from a serialized format, such as JSON or binary, back into its original form. Swift provides multiple protocols allowing users to convert objects and values to and from property lists, JSON, and other flat binary representations.

Deserialization can also introduce unsuspecting security vulnerabilities in a user’s codebase that attackers could exploit. This blog will detail deserialization vulnerabilities in Swift that can occur when using the popular APIs, NScoding and NSSecureCoding, and how to prevent it properly.

NSCoding

NSCoding is a protocol Apple provides that can be used for serialization and deserialization of data. The default protocol to serialize and deserialize data on NSCoding is vulnerable to object substitution attacks — which could allow an attacker to leverage remote code execution. 

Take the following class, which implements NSCoding with two methods — initWithCoder: and encodeWithCoder:. Classes conforming to NSCoding can be serialized and deserialized into data that can be archived to a disk or distributed across a network.

 

import Foundation

class Employee: NSObject, NSCoding {
    var name: String
    var role: String

    init(name: String, role: String) {
        self.name = name
        self.role = role
    }

    required convenience init?(coder aDecoder: NSCoder) {
        guard let name = aDecoder.decodeObject(forKey: "name") as? String,
              let role = aDecoder.decodeObject(forKey: "role") as? String else {
            return nil
        }

        self.init(name: name, role: role)
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
        aCoder.encode(role, forKey: "role")
    }
}
Enter fullscreen mode Exit fullscreen mode

The above class conforms to the NSCoding protocol and has two properties — name and role. This class also implements the required init(coder:) initializer to decode the name and role properties from an archive and the encode(with:) method to encode the name and role properties into an archive.

An employee object can then be created and archived to a file using NSKeyedArchiver.

// Archive the Employee object to a file
let person = Employee(name: "John", role: "consultant")
let fileURL = URL(fileURLWithPath: "/tmp/file")
NSKeyedArchiver.archiveRootObject(person, toFile: fileURL.path)
Enter fullscreen mode Exit fullscreen mode

This can then be unarchived to the employee object from the file using NSKeyedUnarchiver to print the values of the name and role properties.

// Unarchive the Employee object from the file
if let unarchivedPerson = NSKeyedUnarchiver.unarchiveObject(withFile: "/tmp/file") as? Employee {
    print("Name: \(unarchivedPerson.name), Role: \(unarchivedPerson.role)")
} else {
    print("Failed to unarchive Employee object")
}
Enter fullscreen mode Exit fullscreen mode

This can be potentially exploited in the following way. Assume other classes exist in the codebase, including the following class. This class has a single property called "command" — a string representing a file path. The class also has two methods, "encode(with:)" and "init?(coder:)", which are required to conform to the NSCoding protocol.

The command property is taken from an encoded object and flows into the sink1 function — which executes the property as part of a command.

import Foundation

class ExampleGadget: NSObject, NSCoding {

    let command: String

    internal init(command: String) {
        self.command = command
    }

    func encode(with coder: NSCoder) {
        coder.encode(command, forKey: "command")
    }

    required init?(coder: NSCoder) {
      command = coder.decodeObject(forKey: "command") as! String

        super.init()
        var result = sink1(tainted: command)
        print(result)
    }

    func sink1(tainted: String) -> String {

            let process = Process()
            process.executableURL = URL(fileURLWithPath: "/bin/bash")
            process.arguments = ["-c", tainted]
            let pipe = Pipe()
            process.standardOutput = pipe
            process.launch()
            process.waitUntilExit()
            let data = pipe.fileHandleForReading.readDataToEndOfFile()
            guard let output: String = String(data: data, encoding: .utf8) else { return "" }
        return output

}
}
Enter fullscreen mode Exit fullscreen mode

An attacker can leverage this class to execute code and conduct a deserialization attack that can lead to a command injection.

NSSecureCoding

NSSecureCoding is a secure alternative to NSCoding that provides enhanced security features to safeguard against deserialization attacks. Unlike NSCoding, NSSecureCoding imposes stricter security requirements on the encoded and decoded objects, including the need for a defined class hierarchy and specific security measures to be implemented in the classes themselves. These added measures help prevent the manipulation of serialized data that could result in the creation of objects capable of executing malicious code, thereby enhancing the system's overall security. However, deserialization attacks might still occur depending on how it's used.

Decoding without verifying the class type

In the below example, the code has been changed to conform to NSSecureCoding. The supportSecureCoding property has been set to true, and just like before, the decodeObject method is used to deserialize encoded objects.

import Foundation

class Employee: NSObject, NSSecureCoding {

    public static var supportsSecureCoding = true
    var name: String
    var role: String

    init(name: String, role: String) {
        self.name = name
        self.role = role
    }

    required convenience init?(coder aDecoder: NSCoder) {
        guard let name = aDecoder.decodeObject(forKey: "name") as? String,
              let role = aDecoder.decodeObject(of:Employee.self, forKey: "role") as? String else {
            return nil
        }

        self.init(name: name, role: role)
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
        aCoder.encode(role, forKey: "role")
    }
}
Enter fullscreen mode Exit fullscreen mode

However, as stated in the Apple developer documentation for NSSecureCoding, this technique is potentially unsafe because, by the time you verify the class type, the object has already been constructed — and if this is part of a collection class, potentially inserted into an object graph. Setting supportsSecureCoding to true tags an object as safe to decode. However, no verification is done by NSSecureCoding to verify the type of this object and whether this relates to the relevant employee class.

To remediate this issue, the of key can be specified with decodeObject(of:Employee.self, forKey: "name"), which only decodes objects of the specified class (in this case, employee). This ensures that the name property can only be decoded as a string, not as a maliciously crafted object. Alternatively, the decodeObjectOfClass method can be used — which decodes an object for the key restricted to the specified class. Furthermore, the decodeObjectForKey and decodeTopLevelObjectForKey methods should not be used, which is also affected by this issue and is considered deprecated by Apple. 

supportsSecureCoding set to False

As stated in the Apple developer documentation, you should ensure that this class property's getter returns true when writing a class that supports secure coding. Setting supportsSecureCoding = false still conforms to NSSecureCoding and gives developers a false sense of security while allowing deserialization attacks.

Secure Swift deserialization

In summary, security considerations should be taken into account when deserializing user data using NSCoding and NSSecureCoding. While documentation might make it look like NSSecureCoding prevents deserialization by default, this is not the case, and instances exist where unsafe deserialization is still possible. Whenever using decode functions with NSSecureCoding, ensure that the type of the object being deserialized is verified

References

Example code used in this blog can be found here: https://github.com/snoopysecurity/swift-deserialization-security-primer

Top comments (0)