Skip to main content

Sicherheitsleitfaden zur Deserialisierung in Swift

Artikel von
Headshot of Sam Sanoop

Sam Sanoop

feature java dto

18. Juli 2023

0 Min. Lesezeit

Bei der Deserialisierung werden Daten aus einem serialisierten Format, etwa JSON oder Binärdaten, in ihre ursprüngliche Form zurückgeführt. Swift bietet mehrere Protokolle, mit denen sich Objekte und Werte in Property Lists, JSON und andere flache Binärformate umwandeln und daraus zurückführen lassen.

Deserialisierung kann in der Codebasis auch unbemerkt Sicherheitsschwachstellen verursachen, die Angreifer ausnutzen könnten. Dieser Blogbeitrag erläutert Deserialisierungsschwachstellen in Swift, die bei Verwendung der verbreiteten APIs NSCoding und NSSecureCoding auftreten können, und zeigt, wie Sie sie wirksam verhindern.

NSCoding

NSCoding ist ein von Apple bereitgestelltes Protokoll, das sich zur Serialisierung und Deserialisierung von Daten verwenden lässt. Das standardmäßige Protokoll zur Serialisierung und Deserialisierung von Daten mit NSCoding ist anfällig für Objekt-Ersetzungsangriffe, durch die Angreifer unter Umständen Remote-Codeausführung erreichen können.

Betrachten Sie die folgende Klasse, die NSCoding mit zwei Methoden implementiert: initWithCoder: und encodeWithCoder:. Klassen, die NSCoding entsprechen, können in Daten serialisiert und deserialisiert werden, die sich auf einem Datenträger archivieren oder über ein Netzwerk übertragen lassen.

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")
    }
}

Die obige Klasse entspricht dem NSCoding-Protokoll und verfügt über zwei Eigenschaften: name und role. Außerdem implementiert die Klasse den Initialisierer required init(coder:), um die Eigenschaften name und role aus einem Archiv zu dekodieren, sowie die Methode encode(with:), um diese Eigenschaften in einem Archiv zu kodieren.

Anschließend lässt sich ein Mitarbeiterobjekt erstellen und mithilfe von NSKeyedArchiver in einer Datei archivieren.

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

Anschließend lässt sich das Objekt mithilfe von NSKeyedUnarchiver aus der Datei wieder in ein Mitarbeiterobjekt deserialisieren, um die Werte der Eigenschaften name und role auszugeben.

// 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")
}

Dies lässt sich potenziell wie folgt ausnutzen. Nehmen wir an, dass in der Codebasis weitere Klassen existieren, darunter die folgende. Diese Klasse hat eine einzelne Eigenschaft namens "command" – eine Zeichenfolge, die einen Dateipfad darstellt. Außerdem verfügt sie über die beiden Methoden "encode(with:)" und "init?(coder:)", die für die Konformität mit dem NSCoding-Protokoll erforderlich sind.

Die Eigenschaft command stammt aus einem kodierten Objekt und wird an die Funktion sink1 weitergegeben, die die Eigenschaft als Teil eines Befehls ausführt.

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

}
}

Ein Angreifer kann diese Klasse ausnutzen, um Code auszuführen und einen Deserialisierungsangriff durchzuführen, der zu einer Befehlsinjektion führen kann.

NSSecureCoding

NSSecureCoding ist eine sichere Alternative zu NSCoding und bietet erweiterte Sicherheitsfunktionen zum Schutz vor Deserialisierungsangriffen. Anders als NSCoding stellt NSSecureCoding strengere Sicherheitsanforderungen an kodierte und dekodierte Objekte. Dazu gehören eine festgelegte Klassenhierarchie und bestimmte Sicherheitsmaßnahmen, die in den Klassen selbst implementiert werden müssen. Diese zusätzlichen Maßnahmen tragen dazu bei, die Manipulation serialisierter Daten zu verhindern, durch die Objekte erstellt werden könnten, die schädlichen Code ausführen. So wird die Gesamtsicherheit des Systems erhöht. Je nach Verwendung kann es jedoch weiterhin zu Deserialisierungsangriffen kommen.

Dekodierung ohne Überprüfung des Klassentyps

Im folgenden Beispiel wurde der Code so geändert, dass er NSSecureCoding entspricht. Die Eigenschaft supportSecureCoding wurde auf true gesetzt, und wie zuvor wird die Methode decodeObject zum Deserialisieren kodierter Objekte verwendet.

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")
    }
}

Wie jedoch in der Apple-Entwicklerdokumentation zu NSSecureCoding erläutert, ist dieses Vorgehen potenziell unsicher: Wenn Sie den Klassentyp überprüfen, wurde das Objekt bereits erstellt und – falls es Teil einer Sammlungsklasse ist – möglicherweise schon in einen Objektgraphen eingefügt. Mit supportsSecureCoding auf true wird ein Objekt als sicher zu dekodieren gekennzeichnet. NSSecureCoding überprüft jedoch weder den Typ dieses Objekts noch, ob es zur betreffenden Mitarbeiterklasse gehört.

Um dieses Problem zu beheben, können Sie den Schlüssel of mit decodeObject(of:Employee.self, forKey: "name") angeben. Dadurch werden nur Objekte der angegebenen Klasse dekodiert (in diesem Fall Employee). So wird sichergestellt, dass die Eigenschaft name nur als Zeichenfolge und nicht als manipuliertes Objekt dekodiert werden kann. Alternativ lässt sich die Methode decodeObjectOfClass verwenden, die ein Objekt für den Schlüssel ausschließlich dann dekodiert, wenn es der angegebenen Klasse entspricht. Außerdem sollten Sie die Methoden decodeObjectForKey und decodeTopLevelObjectForKey nicht verwenden: Auch sie sind von diesem Problem betroffen und gelten laut Apple als veraltet.

supportsSecureCoding auf False gesetzt

Wie in der Apple-Entwickler-Dokumentation erläutert, sollten Sie beim Schreiben einer Klasse, die sicheres Coding unterstützt, sicherstellen, dass der Getter dieser Klasseneigenschaft true zurückgibt. Wenn Sie supportsSecureCoding = false setzen, entspricht die Klasse zwar weiterhin NSSecureCoding. Das vermittelt Entwicklerinnen und Entwicklern jedoch ein falsches Sicherheitsgefühl und ermöglicht zugleich Deserialisierungsangriffe.

Sichere Swift-Deserialisierung

Zusammenfassend sollten Sie Sicherheitsaspekte berücksichtigen, wenn Sie Benutzerdaten mit NSCoding und NSSecureCoding deserialisieren. Die Dokumentation erweckt möglicherweise den Eindruck, NSSecureCoding verhindere Deserialisierung standardmäßig. Das ist jedoch nicht der Fall: Auch mit NSSecureCoding kann es zu unsicherer Deserialisierung kommen. Wenn Sie Dekodierfunktionen mit NSSecureCoding verwenden, müssen Sie stets den Typ des zu deserialisierenden Objekts überprüfen.

Referenzen

Den Beispielcode aus diesem Blogbeitrag finden Sie hier: https://github.com/snoopysecurity/swift-deserialization-security-primer

Gepostet in:

Weiterlesen

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSec: Sicherheit, die mit der KI-Entwicklung Schritt hält

KI-gestützte Entwicklung ist schneller als herkömmliche Sicherheitsabläufe. Snyks Agentic AppSec setzt Sicherheitsagenten ein, um Schwachstellen kontinuierlich zu finden, zu beheben und zu überprüfen – während Menschen die Maßstäbe festlegen.

feature insights context
Blog

Autonome Angriffe sind bereits Realität. Die Verteidigung muss Schritt halten.

Autonome Angreifer verkürzen das Zeitfenster für die Verteidigung. Erfahren Sie, wie kontinuierliches Erkennen, Beheben, Validieren und Verhindern Sicherheitsteams helfen kann, Schritt zu halten.

illustration hero ai
Blog

Der KI-Hurrikan ist da

KI beschleunigt gleichermaßen die Softwareentwicklung und Cyberangriffe. Führungskräfte müssen Agents und Code von Anfang an absichern, zur Laufzeit Kontrollen durchsetzen und Abwehrmaßnahmen unabhängig validieren.