Skip to main content

Premiers repères sur la sécurité de la désérialisation en Swift

Écrit par
Headshot of Sam Sanoop

Sam Sanoop

feature java dto

18 juillet 2023

0 minutes de lecture

La désérialisation consiste à convertir des données depuis un format sérialisé, comme JSON ou un format binaire, pour les restituer sous leur forme d’origine. Swift propose plusieurs protocoles qui permettent de convertir des objets et des valeurs en listes de propriétés, en JSON et en d’autres représentations binaires plates, et inversement.

La désérialisation peut également introduire à l’insu des développeurs des vulnérabilités de sécurité dans leur base de code, que des attaquants pourraient exploiter. Cet article détaille les vulnérabilités de désérialisation en Swift qui peuvent survenir lors de l’utilisation des API populaires NScoding et NSSecureCoding, et explique comment les prévenir correctement.

NSCoding

NSCoding est un protocole fourni par Apple qui peut être utilisé pour sérialiser et désérialiser des données. Le protocole utilisé par défaut pour sérialiser et désérialiser des données avec NSCoding est vulnérable aux attaques par substitution d’objet, qui pourraient permettre à un attaquant d’exécuter du code à distance.

Considérez la classe suivante, qui implémente NSCoding avec deux méthodes : initWithCoder: et encodeWithCoder:. Les classes conformes à NSCoding peuvent être sérialisées et désérialisées en données pouvant être archivées sur un disque ou transmises sur un réseau.

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

La classe ci-dessus est conforme au protocole NSCoding et possède deux propriétés : name et role. Elle implémente également l’initialiseur required init(coder:) pour décoder les propriétés name et role à partir d’une archive, ainsi que la méthode encode(with:) pour encoder ces propriétés dans une archive.

Un objet employee peut ensuite être créé et archivé dans un fichier à l’aide de 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)

Il est ensuite possible de désarchiver l’objet employee à partir du fichier à l’aide de NSKeyedUnarchiver afin d’afficher les valeurs des propriétés name et role.

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

Cette situation peut être exploitée de la manière suivante. Supposons que la base de code contienne d’autres classes, notamment la classe suivante. Celle-ci possède une seule propriété appelée « command », une chaîne représentant un chemin de fichier. Elle possède également deux méthodes, « encode(with:) » et « init?(coder:) », requises pour être conforme au protocole NSCoding.

La propriété command est extraite d’un objet encodé, puis transmise à la fonction sink1, qui exécute cette propriété dans le cadre d’une commande.

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

}
}

Un attaquant peut exploiter cette classe pour exécuter du code et mener une attaque par désérialisation susceptible d’entraîner une injection de commande.

NSSecureCoding

NSSecureCoding est une alternative sécurisée à NSCoding qui offre des fonctionnalités de sécurité renforcées pour se prémunir contre les attaques par désérialisation. Contrairement à NSCoding, NSSecureCoding impose des exigences de sécurité plus strictes aux objets encodés et décodés, notamment la définition d’une hiérarchie de classes et la mise en œuvre de mesures de sécurité spécifiques dans les classes elles-mêmes. Ces mesures supplémentaires contribuent à empêcher la manipulation des données sérialisées, qui pourrait entraîner la création d’objets capables d’exécuter du code malveillant, et renforcent ainsi la sécurité globale du système. Toutefois, des attaques par désérialisation peuvent encore survenir selon la manière dont le protocole est utilisé.

Décoder sans vérifier le type de classe

Dans l’exemple ci-dessous, le code a été modifié pour être conforme à NSSecureCoding. La propriété supportSecureCoding est définie sur true et, comme précédemment, la méthode decodeObject est utilisée pour désérialiser les objets encodés.

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

Toutefois, comme l’indique la documentation destinée aux développeurs Apple sur NSSecureCoding, cette technique peut être dangereuse, car lorsque vous vérifiez le type de classe, l’objet a déjà été construit et, s’il fait partie d’une classe de collection, peut déjà avoir été inséré dans un graphe d’objets. Définir supportsSecureCoding sur true indique qu’un objet peut être décodé de manière sécurisée. Cependant, NSSecureCoding ne vérifie ni le type de cet objet ni s’il correspond à la classe employee concernée.

Pour corriger ce problème, vous pouvez spécifier la clé of avec decodeObject(of:Employee.self, forKey: "name"), qui ne décode que les objets de la classe spécifiée (ici, employee). Cela garantit que la propriété name ne peut être décodée qu’en tant que chaîne, et non comme un objet conçu à des fins malveillantes. Vous pouvez également utiliser la méthode decodeObjectOfClass, qui décode l’objet associé à la clé uniquement s’il appartient à la classe spécifiée. De plus, les méthodes decodeObjectForKey et decodeTopLevelObjectForKey ne doivent pas être utilisées : elles sont également concernées par ce problème et Apple les considère comme obsolètes.

supportsSecureCoding défini sur False

Comme l’indique la documentation destinée aux développeurs Apple, lorsque vous écrivez une classe compatible avec le codage sécurisé, vous devez vous assurer que l’accesseur de cette propriété de classe renvoie true. Définir supportsSecureCoding = false permet tout de même d’être conforme à NSSecureCoding et donne aux développeurs un faux sentiment de sécurité, tout en laissant la porte ouverte aux attaques par désérialisation.

Désérialisation sécurisée en Swift

En résumé, il convient de tenir compte des enjeux de sécurité lors de la désérialisation de données utilisateur à l’aide de NSCoding et NSSecureCoding. La documentation peut laisser penser que NSSecureCoding empêche la désérialisation par défaut, mais ce n’est pas le cas : la désérialisation non sécurisée reste possible dans certaines situations. Lorsque vous utilisez des fonctions de décodage avec NSSecureCoding, veillez à vérifier le type de l’objet désérialisé.

Références

Vous trouverez ici le code d’exemple utilisé dans cet article : https://github.com/snoopysecurity/swift-deserialization-security-primer

Publié dans: