WWDC Quick Look 💓 By SwiftGGTeam
Secure your app: threat modeling and anti-patterns

Secure your app: threat modeling and anti-patterns

元の動画を見る

ハイライト

Apple は、パス トラバーサル、セッション状態、デシリアライゼーション ホワイトリスト、バッファ長、整数オーバーフローの 5 つのコード スニペットを使用して、アプリに入る各外部入力のチェックポイントに脅威モデリングが適用されることを示しています。

主要内容

セキュリティの問題が暗号化アルゴリズムのレベルでのみ発生することはほとんどありません。より一般的な状況は、アプリが外部入力を内部データとして扱うことです。リモート エンドから送信されたファイル名、別のデバイスから送信されたセッション メッセージ、CloudKit レコードのバイナリ フィールド、および逆シリアル化フェーズ中に宣言されたクラス コレクションはすべて、攻撃のエントリ ポイントになる可能性があります。

このセッションの内容は、脅威モデリングをコードレベルの問題に分解します。開発者はまず、データがアプリに入る場所を見つけてから、次の 3 つの質問をします。この値によってファイル パスが変更されるかどうか、ステート マシンをプッシュできるかどうか、ランタイムによって作成されるべきではないオブジェクトが作成されるかどうか、メモリ操作に入る前に長さと数量が検証されるかどうかです。

Apple が提供する保護方法も非常に簡単です。ファイル名は最初に次のように収束しますlastPathComponent; セッション メッセージは、セッション、状態、送信者を同時に一致させる必要があります。動的クラスコレクションはプロトコル制約によって生成されます。固定サイズのメモリは、タイプと長さをチェックする前にコピーされます。量からバイトを計算するときに使用されますos_mul_overflow。これらのチェックは小規模に見えるかもしれませんが、攻撃者が信頼境界を越えることができるかどうかを決定します。

詳細

1. パス トラバーサル: 外部ファイル名を 1 つの安全なファイル名に統合する

(16:34) ファイル インポート機能でよくあるエラーは、リモート エンドからデータを転送することです。nameローカル ディレクトリの後ろに直接移動します。最初に採取した断片lastPathComponent、空の文字列を拒否します。.そして..、最後に一時ディレクトリに書き込みます。

func handleIncomingFile(_ incomingResourceURL: URL, with name: String, from fromID: String) {
    guard
         case let safeFileName = name.lastPathComponent,
         safeFileName.count > 0,
         safeFileName != "..", safeFileName != "." else { return }

    let destinationFileURL = URL(fileURLWithPath: NSTemporaryDirectory())
                                 .appendingPathComponent(safeFileName)

    // Copy the file into a temporary directory
    try! FileManager.default.copyItem(at: incomingResourceURL, to: destinationFileURL)

}

キーポイント:

  • name.lastPathComponent外部入力に含まれるディレクトリ階層を削除し、最後のパス コンポーネントのみを残します。 -safeFileName.count > 0無意味な宛先パスが生成されるのを避けるために、空のファイル名を拒否します。 -safeFileName != ".."そしてsafeFileName != "."現在のディレクトリと親ディレクトリの 2 つの特別なパス コンポーネントを拒否します。 -NSTemporaryDirectory()インポートされたファイルを一時ディレクトリに制限してから渡しますappendingPathComponentチェックされたファイル名を結合します。

2. ステータス管理: リモート メッセージは現在のステータスと送信者と一致する必要があります。

(22:26) セッション招待受諾メッセージはリモート エンドから送信されるため、それだけに依存することはできません。sessionIdentifier接続ステータスを「接続済み」に切り替えるだけです。このコードは、セッションが存在するかどうか、ステータスがまだ招待中かどうか、送信者が招待リストに含まれているかどうかもチェックします。

func handleSessionInviteAccepted(with message: RemoteMessage, from fromID: String) {
    guard session = sessionsByIdentifier[message.sessionIdentifier],
          session.state == .inviting,
          session.invitedFromIdentifiers.contains(fromID) else { return }

    session.state = .connected
    session.setupSocket(to: fromID) { socket in
        cameraController.send(to: socket)
    }
}

キーポイント:

  • sessionsByIdentifier[message.sessionIdentifier]まず、メッセージが既知のセッションを参照していることを確認します。 -session.state == .inviting状態の移行を法的段階に制限し、古いメッセージは自由に状態を進めることができません。 -session.invitedFromIdentifiers.contains(fromID)アクションを招待された送信者にバインドします。 -session.state = .connectedそしてsetupSocket3 つのチェックすべてに合格した後にのみ実行されます。

3. 逆シリアル化のホワイトリスト: 動的コレクションも明示的なルールから生成する必要があります

(30:56) 動的ビルドが必要ですallowedClassesフラグメントは、任意のクラス名を受け入れる代わりに、ランタイム クラス リストを走査し、特定のプロトコルに準拠するクラスのみを収集します。結果のセットにはまだ境界があります。

NSSet *classesWhichConformToProtocol(Protocol *protocol) {
    NSMutableSet *conformingClasses = [NSMutableSet set];

    unsigned int classesCount = 0;
    Class *classes = objc_copyClassList(&classesCount);
    if (classes != NULL) {
        for (unsigned int i = 0; i < classesCount; i++) {
            if (class_conformsToProtocol(classes[i], protocol)) {
                [conformingClasses addObject: classes[i]];
            }
        }
        free(classes);
    }
    return conformingClasses;
}

キーポイント:

  • objc_copyClassList現在のプロセス内のクラスのリストを Objective-C ランタイムから取得します。 -class_conformsToProtocol(classes[i], protocol)ホワイトリストの条件です。プロトコルに準拠することが宣言されたクラスのみがコレクションに追加されます。 -free(classes)補助チェック コードによって引き起こされるメモリ リークを回避するために、ランタイムによって返されたクラス配列を解放します。
  • 戻り値はNSSet、後続の安全な復号化プロセスで許可されるクラスのセットとして適しています。

4. バッファ オーバーフロー: 固定長フィールドをコピーする前に型と長さを確認する

(34:23) CloudKit レコードのフィールドは外部ストレージから取得されます。バンドルNSData固定サイズにコピーする_uuidこれを行う前に、フラグメントはまずフィールドが存在すること、正しいタイプであること、およびターゲット バッファ サイズと等しい長さを持っていることを確認します。

@implementation
- (BOOL)unpackTeaClubRecord:(CKRecord *)record {
    ...
    NSData *data = [record objectForKey:@"uuid"];
    if (data == nil ||
        ![data isKindOfClass:[NSData class]] ||
        data.length != sizeof(_uuid)) {
        return NO;
    }
    memcpy(&_uuid, data.bytes, data.length);
    ...

キーポイント:

  • data == nil欠落しているフィールドは拒否され、後続のメモリ操作では空のオブジェクトが処理されません。 -isKindOfClass:[NSData class]他のオブジェクトがバイナリ バッファとして扱われないように、フィールド タイプを確認してください。 -data.length != sizeof(_uuid)入力長をターゲット バッファ サイズにバインドします。 -memcpy長さチェック、検証されたデータからコピーされたバイト数の後にのみ実行されます。data.length

5. 整数オーバーフロー: 文字列を作成する前にバイト数を安全に計算します。

(36:06) 文字列長フィールドもレコードから取得されます。フラグメントは直接使用されませんcount * sizeof(unichar)、ただし使用しますos_mul_overflowバイト数を数えて置くNSData.length文字列を計算結果に合わせて初期化します。

@implementation
- (BOOL)unpackTeaClubRecord:(CKRecord *)record {
    ...
    NSData *name = [record objectForKey:@"name"];
    int32_t count = [[record objectForKey:@"nameCount"] unsignedIntegerValue];
    int32_t byteCount = 0;
    if (name == nil ||
        ![name isKindOfClass:[NSData class]] ||
        os_mul_overflow(count, sizeof(unichar), &byteCount) ||
        name.length != byteCount) {
        return NO;
    }
    _name = [[NSString alloc] initWithCharacters:name.bytes
                                          length:count];
    ...

キーポイント:

  • nameそしてnameCountこれは同じレコードからのものであり、使用する前に外部入力として扱う必要があります。 -os_mul_overflow(count, sizeof(unichar), &byteCount)乗算結果を入力しますbyteCount事前にオーバーフローを確認してください。 -name.length != byteCountバイトバッファ長と文字数が一致していることを確認してください。 -initWithCharacters:length:不完全なバッファが文字列として解釈されるのを避けるために、バイト数の検証後にのみ呼び出されます。

重要ポイント

  1. ファイルをセーフティ ネットにインポートします 対処法: AirDrop、ドキュメントピッカー、共有拡張機能、アプリ内のダウンロードタスクのファイル名をクリーニング機能に統一します。 実行する価値がある理由: パス トラバーサル スニペットでは、外部ファイル名をパス スプライシングに入力する前に、ディレクトリ階層を削除し、特別なパス コンポーネントを拒否する必要があることを説明しています。 開始方法: 1 つ書いてくださいsafeLocalFileNameヘルパー、空の文字列をオーバーライド、...../../name、絶対パスと長いファイル名をテストし、ファイルのコピーを一時ディレクトリまたはアプリのプライベート コンテナーに制限します。

  2. リモート メッセージ ステート マシン 何をすべきか: コラボレーション、スクリーンキャスト、インスタント メッセージング、またはデバイス相互接続機能の状態テーブルを作成し、各リモート メッセージがトリガーできる状態遷移をリストします。 実行する価値がある理由: セッションの例では、セッションが存在し、状態が.inviting、送信者は招待リストに含まれており、項目が 1 つ欠けていてもソケットを確立できません。 開始方法: 置くstatemessageTypesenderID現在の権限はガード機能にマージされ、単体テストでは重複メッセージ、古いメッセージ、不明な送信者がカバーされます。

  3. 逆シリアル化によりクラス レジストリが可能 対処方法: デコードが許可されているすべてのモデル クラスを明示的なプロトコルまたはレジストリに配置し、呼び出しサイトがクラス名リストを一時的に結合できないようにします。 実行する価値がある理由:classesWhichConformToProtocolフラグメントはプロトコルを使用して動的コレクションを制限し、柔軟性を維持しながら境界を監査可能にします。 開始方法: 安全なデコードにのみ使用されるマーカー プロトコルを定義し、デコードする必要があるクラスにそのプロトコルに準拠していることを明示的に宣言させ、デコード エントリでそれを生成します。NSSet

  4. CloudKit フィールド解凍検証レイヤー やるべきこと:のためにCKRecordその中のバイナリ フィールド、カウント フィールド、文字列フィールドに対して統合されたアンパック関数を作成します。 実行する価値がある理由: 両方の CloudKit スニペットが導入されていますmemcpyまたはNSString初期化前の確認NSData型、正確な長さ、および乗算オーバーフロー。 開始方法: まず、UUID や名前などのフィールドを次のように変更します。readUUID(record:key:)readUTF16Name(record:dataKey:countKey:)このような小さな機能を実行して、奇形記録のテストを完了します。

  5. PR レベルの脅威モデリングのチェックリスト 動作: 外部入力を導入する各 PR は、ファイル パスを書き込むか、状態を進めるか、オブジェクトをデコードするか、メモリを長さごとにコピーするかという 4 つの質問に答えます。 実行する価値がある理由: このセッションのコード スニペットは、セキュリティ レビューをいくつかの特定のエントリに圧縮しているため、「セキュリティ」一般について議論するよりも実装が簡単です。 開始方法: これら 4 つのチェックを PR テンプレートに追加し、関連するコードに少なくとも 1 つの悪意のある入力テストが伴うことを要求します。

関連セッション

コメント

GitHub Issues · utterances