Skip to main content

Guia básico de segurança na desserialização em Swift

Escrito por
Headshot of Sam Sanoop

Sam Sanoop

feature java dto

18 de julho de 2023

0 minutos de leitura

Desserialização é o processo de converter dados de um formato serializado, como JSON ou binário, de volta à forma original. O Swift oferece vários protocolos que permitem converter objetos e valores de e para listas de propriedades, JSON e outras representações binárias planas.

A desserialização também pode introduzir vulnerabilidades de segurança inesperadas no código de uma pessoa usuária, que podem ser exploradas por atacantes. Este artigo detalha vulnerabilidades de desserialização em Swift que podem ocorrer ao usar as APIs populares, NScoding e NSSecureCoding, além de mostrar como preveni-las corretamente.

NSCoding

NSCoding é um protocolo fornecido pela Apple que pode ser usado para serializar e desserializar dados. O protocolo padrão para serializar e desserializar dados com NSCoding é vulnerável a ataques de substituição de objetos, que podem permitir que um atacante execute código remotamente. 

Considere a classe a seguir, que implementa NSCoding com dois métodos: initWithCoder: e encodeWithCoder:. Classes que adotam NSCoding podem ser serializadas e desserializadas em dados que podem ser arquivados em disco ou distribuídos pela rede. 

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

A classe acima adota o protocolo NSCoding e tem duas propriedades: name e role. Ela também implementa o inicializador required init(coder:) para decodificar as propriedades name e role de um arquivo e o método encode(with:) para codificá-las em um arquivo.

Em seguida, é possível criar um objeto employee e arquivá-lo em um arquivo usando 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)

Depois, é possível desarquivar esse objeto employee do arquivo usando NSKeyedUnarchiver e imprimir os valores das propriedades name e 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")
}

Isso pode ser explorado da seguinte maneira. Suponha que existam outras classes na base de código, incluindo a classe a seguir. Ela tem uma única propriedade chamada "command", uma string que representa um caminho de arquivo. A classe também tem dois métodos, "encode(with:)" e "init?(coder:)", necessários para adotar o protocolo NSCoding.

A propriedade command é obtida de um objeto codificado e direcionada para a função sink1, que a executa como parte de um comando.

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

}
}

Um atacante pode explorar essa classe para executar código e realizar um ataque de desserialização que pode levar à injeção de comandos.

NSSecureCoding

NSSecureCoding é uma alternativa mais segura ao NSCoding, com recursos aprimorados para proteger contra ataques de desserialização. Ao contrário do NSCoding, NSSecureCoding impõe requisitos de segurança mais rigorosos aos objetos codificados e decodificados, incluindo a necessidade de uma hierarquia de classes definida e de medidas de segurança específicas implementadas nas próprias classes. Essas medidas ajudam a impedir a manipulação de dados serializados que poderia resultar na criação de objetos capazes de executar código malicioso, reforçando a segurança geral do sistema. No entanto, ainda podem ocorrer ataques de desserialização, dependendo de como o protocolo é usado.

Decodificação sem verificar o tipo da classe

No exemplo abaixo, o código foi alterado para adotar NSSecureCoding. A propriedade supportSecureCoding foi definida como true e, assim como antes, o método decodeObject é usado para desserializar objetos codificados.

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

No entanto, como informa a documentação para desenvolvedores da Apple sobre NSSecureCoding, essa técnica pode não ser segura porque, quando o tipo da classe é verificado, o objeto já foi construído e, se fizer parte de uma classe de coleção, pode já ter sido inserido em um grafo de objetos. Definir supportsSecureCoding como true marca um objeto como seguro para decodificação. Porém, NSSecureCoding não verifica o tipo desse objeto nem se ele corresponde à classe employee esperada.

Para corrigir esse problema, especifique a chave of com decodeObject(of:Employee.self, forKey: "name"), que decodifica apenas objetos da classe especificada (neste caso, employee). Isso garante que a propriedade name seja decodificada apenas como uma string, e não como um objeto criado de forma maliciosa. Outra opção é usar o método decodeObjectOfClass, que decodifica o objeto associado à chave, restringindo-o à classe especificada. Além disso, não use os métodos decodeObjectForKey e decodeTopLevelObjectForKey: eles também são afetados por esse problema e foram considerados obsoletos pela Apple. 

supportsSecureCoding definido como False

Como informa a documentação para desenvolvedores da Apple, ao escrever uma classe compatível com codificação segura, você deve garantir que o getter dessa propriedade da classe retorne true. Definir supportsSecureCoding = false ainda permite adotar NSSecureCoding e dá a quem desenvolve uma falsa sensação de segurança, deixando ataques de desserialização possíveis.

Desserialização segura em Swift

Em resumo, é preciso levar em conta a segurança ao desserializar dados de usuários usando NSCoding e NSSecureCoding. Embora a documentação possa dar a entender que NSSecureCoding impede a desserialização insegura por padrão, isso não é verdade: ainda há casos em que ela é possível. Sempre que usar funções de decodificação com NSSecureCoding, verifique o tipo do objeto que está sendo desserializado.

Referências

O código de exemplo usado neste artigo está disponível aqui: https://github.com/snoopysecurity/swift-deserialization-security-primer

Leia mais

blog feature snyk container custom base recommendations
Blog

Conheça o Evo Agentic AppSec: segurança que acompanha o ritmo do desenvolvimento com IA

O desenvolvimento impulsionado por IA está ultrapassando os fluxos de trabalho tradicionais de segurança. O Agentic AppSec da Snyk usa agentes de segurança para encontrar, corrigir e verificar vulnerabilidades continuamente, enquanto as pessoas definem os critérios.

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

illustration hero ai
Blog

O furacão da IA chegou

A IA está acelerando tanto a criação de software quanto os ataques cibernéticos. As lideranças devem proteger agentes e código desde o início, aplicar controles em tempo de execução e validar as defesas de forma independente.