Swiftのデシリアライズセキュリティ入門
Sam Sanoop
2023年7月18日
0 分で読めますデシリアライズとは、JSONやバイナリなどのシリアライズ形式のデータを、元の形式に戻す処理です。Swiftには、プロパティリストやJSON、その他のフラットなバイナリ表現との間でオブジェクトや値を変換できる複数のプロトコルが用意されています。
デシリアライズによって、ユーザーのコードベースに気づきにくいセキュリティ脆弱性が生じ、攻撃者に悪用される可能性もあります。このブログでは、広く使われているAPIのNScodingとNSSecureCodingを使用する際にSwiftで発生しうるデシリアライズの脆弱性と、それを適切に防ぐ方法について詳しく解説します。
NSCoding
NSCodingは、データのシリアライズとデシリアライズに使用できる、Appleが提供するプロトコルです。NSCodingでデータをシリアライズおよびデシリアライズする標準的な方法には、オブジェクト置換攻撃に対する脆弱性があり、攻撃者にリモートコード実行を許す可能性があります。
次のクラスは、initWithCoder:とencodeWithCoder:という2つのメソッドを実装してNSCodingに準拠しています。NSCodingに準拠するクラスは、シリアライズおよびデシリアライズして、ディスクに保存したりネットワーク経由で配布したりできるデータに変換できます。
上記のクラスはNSCodingプロトコルに準拠しており、nameとroleという2つのプロパティを持ちます。また、アーカイブからnameとroleのプロパティをデコードするrequired init(coder:)イニシャライザーと、nameとroleのプロパティをアーカイブにエンコードするencode(with:)メソッドを実装しています。
次に、NSKeyedArchiverを使ってemployeeオブジェクトを作成し、ファイルにアーカイブできます。
その後、NSKeyedUnarchiverを使ってファイルからemployeeオブジェクトを復元し、nameとroleのプロパティの値を出力できます。
次のような方法で悪用される可能性があります。コードベースに、次のクラスを含む他のクラスがあるとします。このクラスは、ファイルパスを表す文字列である「command」という単一のプロパティを持ちます。また、NSCodingプロトコルへの準拠に必要な「encode(with:)」と「init?(coder:)」という2つのメソッドも持ちます。
commandプロパティはエンコードされたオブジェクトから取得され、sink1関数に渡されます。この関数は、プロパティをコマンドの一部として実行します。
攻撃者はこのクラスを利用してコードを実行し、コマンドインジェクションにつながるデシリアライズ攻撃を仕掛けることができます。
NSSecureCoding
NSSecureCodingはNSCodingのセキュアな代替手段であり、デシリアライズ攻撃を防ぐための強化されたセキュリティ機能を備えています。NSCodingとは異なり、NSSecureCodingでは、定義されたクラス階層や、クラス自体に実装すべき特定のセキュリティ対策など、エンコードおよびデコードするオブジェクトに対してより厳格なセキュリティ要件が課されます。こうした対策により、悪意のあるコードを実行できるオブジェクトの作成につながるシリアライズ済みデータの改ざんを防ぎ、システム全体のセキュリティを高めます。ただし、使い方によってはデシリアライズ攻撃が発生する可能性があります。
クラス型を検証せずにデコードする
以下の例では、NSSecureCodingに準拠するようコードを変更しています。supportSecureCodingプロパティをtrueに設定し、以前と同様にdecodeObjectメソッドを使ってエンコードされたオブジェクトをデシリアライズします。
しかし、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



