Skip to main content

Swiftのデシリアライズセキュリティ入門

著者
Headshot of Sam Sanoop

Sam Sanoop

feature java dto

2023年7月18日

0 分で読めます

デシリアライズとは、JSONやバイナリなどのシリアライズ形式のデータを、元の形式に戻す処理です。Swiftには、プロパティリストやJSON、その他のフラットなバイナリ表現との間でオブジェクトや値を変換できる複数のプロトコルが用意されています。

デシリアライズによって、ユーザーのコードベースに気づきにくいセキュリティ脆弱性が生じ、攻撃者に悪用される可能性もあります。このブログでは、広く使われているAPIのNScodingとNSSecureCodingを使用する際にSwiftで発生しうるデシリアライズの脆弱性と、それを適切に防ぐ方法について詳しく解説します。

NSCoding

NSCodingは、データのシリアライズとデシリアライズに使用できる、Appleが提供するプロトコルです。NSCodingでデータをシリアライズおよびデシリアライズする標準的な方法には、オブジェクト置換攻撃に対する脆弱性があり、攻撃者にリモートコード実行を許す可能性があります。

次のクラスは、initWithCoder:とencodeWithCoder:という2つのメソッドを実装してNSCodingに準拠しています。NSCodingに準拠するクラスは、シリアライズおよびデシリアライズして、ディスクに保存したりネットワーク経由で配布したりできるデータに変換できます。

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

上記のクラスはNSCodingプロトコルに準拠しており、nameとroleという2つのプロパティを持ちます。また、アーカイブからnameとroleのプロパティをデコードするrequired init(coder:)イニシャライザーと、nameとroleのプロパティをアーカイブにエンコードするencode(with:)メソッドを実装しています。

次に、NSKeyedArchiverを使ってemployeeオブジェクトを作成し、ファイルにアーカイブできます。

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

その後、NSKeyedUnarchiverを使ってファイルからemployeeオブジェクトを復元し、nameと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")
}

次のような方法で悪用される可能性があります。コードベースに、次のクラスを含む他のクラスがあるとします。このクラスは、ファイルパスを表す文字列である「command」という単一のプロパティを持ちます。また、NSCodingプロトコルへの準拠に必要な「encode(with:)」と「init?(coder:)」という2つのメソッドも持ちます。

commandプロパティはエンコードされたオブジェクトから取得され、sink1関数に渡されます。この関数は、プロパティをコマンドの一部として実行します。

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

}
}

攻撃者はこのクラスを利用してコードを実行し、コマンドインジェクションにつながるデシリアライズ攻撃を仕掛けることができます。

NSSecureCoding

NSSecureCodingはNSCodingのセキュアな代替手段であり、デシリアライズ攻撃を防ぐための強化されたセキュリティ機能を備えています。NSCodingとは異なり、NSSecureCodingでは、定義されたクラス階層や、クラス自体に実装すべき特定のセキュリティ対策など、エンコードおよびデコードするオブジェクトに対してより厳格なセキュリティ要件が課されます。こうした対策により、悪意のあるコードを実行できるオブジェクトの作成につながるシリアライズ済みデータの改ざんを防ぎ、システム全体のセキュリティを高めます。ただし、使い方によってはデシリアライズ攻撃が発生する可能性があります。

クラス型を検証せずにデコードする

以下の例では、NSSecureCodingに準拠するようコードを変更しています。supportSecureCodingプロパティをtrueに設定し、以前と同様にdecodeObjectメソッドを使ってエンコードされたオブジェクトをデシリアライズします。

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

しかし、Appleの開発者向けドキュメントのNSSecureCodingに記載されているように、この手法は安全でない可能性があります。クラス型を検証する時点では、オブジェクトはすでに構築されており、コレクションクラスの一部であれば、オブジェクトグラフに挿入されている可能性もあるためです。supportsSecureCodingをtrueに設定すると、オブジェクトはデコードしても安全であると示されます。しかし、NSSecureCodingは、このオブジェクトの型や、それが該当するemployeeクラスに関連するかどうかを検証しません。

この問題を修正するには、ofキーをdecodeObject(of:Employee.self, forKey: "name")のように指定します。これにより、指定したクラス(この場合はemployee)のオブジェクトのみがデコードされます。そのため、nameプロパティは文字列としてのみデコードされ、悪意を持って作成されたオブジェクトとしてデコードされることはありません。また、decodeObjectOfClassメソッドを使うと、指定したクラスに制限してキーのオブジェクトをデコードできます。さらに、この問題の影響を受け、Appleによって非推奨とされているdecodeObjectForKeyメソッドとdecodeTopLevelObjectForKeyメソッドは使用しないでください。

supportsSecureCodingをFalseに設定

Appleの開発者向けドキュメントに記載されているように、セキュアコーディングをサポートするクラスを作成する場合は、このクラスプロパティのゲッターがtrueを返すようにしてください。supportsSecureCoding = falseに設定してもNSSecureCodingには準拠しますが、開発者に誤った安心感を与え、デシリアライズ攻撃を可能にしてしまいます。

Swiftで安全にデシリアライズする

まとめると、NSCodingやNSSecureCodingを使ってユーザーデータをデシリアライズする際は、セキュリティ上の考慮が必要です。ドキュメントからは、NSSecureCodingがデフォルトでデシリアライズ攻撃を防ぐように見えるかもしれませんが、実際はそうではなく、安全でないデシリアライズが依然として可能なケースがあります。NSSecureCodingでデコード関数を使用する際は、デシリアライズされるオブジェクトの型が検証されていることを必ず確認してください。

参考資料

このブログで使用したコード例はこちらをご覧ください: https://github.com/snoopysecurity/swift-deserialization-security-primer

続きを読む

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSecのご紹介:AI開発のスピードに追随するセキュリティ

AIを活用した開発は、従来のセキュリティワークフローを上回るスピードで進んでいます。SnykのAgentic AppSecは、セキュリティエージェントが脆弱性を継続的に検出、修正、検証し、人が基準を定められるようにします。

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

illustration hero ai
Blog

AIハリケーンが到来

AIはソフトウェア開発とサイバー攻撃の双方を加速させています。リーダーは、エージェントとコードを開発の初期段階から保護し、実行時に制御を徹底するとともに、防御策を独立して検証しなければなりません。