クラウド Mac で検証する iOS ZIP 展開のパスと容量制限

クラウド Mac で検証する iOS ZIP 展開のパスと容量制限

クラウド Mac で検証する iOS ZIP 展開のパスと容量制限

ユーザーが ZIP を取り込み、アプリには成功と表示されたのに、ファイルが取り込み先ディレクトリの外に書き出されていた。この問題の原因は、たいてい圧縮アルゴリズムではなく、エントリ名をそのまま出力パスに連結していることにあります。この処理をクラウド Mac で検証する際は、まずパスのルールをテストし、その後でプロジェクトの展開ライブラリを組み込みます。悪意のある入力のテストに実際のユーザーデータを使ったり、テストでソースコードのディレクトリに書き込んだりしてはいけません。

取り込みの境界を先に定義する

ここでは通常のファイルとディレクトリだけを受け入れ、シンボリックリンクと不明な種類のエントリは拒否します。取り込みのたびに専用のステージングディレクトリを作成し、すべての検証が完了してからアプリのデータディレクトリへ移動します。どれか一つのエントリでも処理に失敗した場合は、ステージングデータをすべて削除し、「成功した分だけ」を残さないようにします。

実装上の上限として、エントリ数は最大千件、ファイル単位では最大二十メガバイト、展開後の合計サイズは最大百メガバイトと定めます。これはポリシーの例であり、システムの制限ではありません。実際の要件に合わせてチームで調整してください。事前検証では ZIP のディレクトリ情報に記載された申告値を使い、実際の書き込み時には実バイト数を累積します。この二段階のチェックは、どちらも省略できません。

サンドボックスは、ZIP 展開処理の安全性を保証する境界ではありません。アプリのコンテナ外に書き込めなくても、悪意のあるエントリがコンテナ内のデータベースや設定を上書きする可能性があります。

実行可能なパス事前検証を書く

以下のコードを zip-path-check.swift として保存し、クラウド Mac で xcrun swift zip-path-check.swift を実行します。このコードはエントリ名だけを検証し、ZIP 自体は読み込みません。プロジェクトでは、展開ライブラリが提供するエントリ名と種類を同じポリシーに渡す必要があります。コマンドが成功しただけで、取り込み処理全体が安全になったと判断してはいけません。

import Foundation

enum PathError: Error {
    case invalid
}

func safeComponents(_ name: String) throws -> [String] {
    guard !name.isEmpty,
          !name.hasPrefix("/"),
          !name.contains("\\"),
          !name.contains(":"),
          !name.unicodeScalars.contains(where: {
              CharacterSet.controlCharacters.contains($0)
          }) else {
        throw PathError.invalid
    }

    let body = name.hasSuffix("/") ? String(name.dropLast()) : name
    let parts = body.split(
        separator: "/",
        omittingEmptySubsequences: false
    ).map(String.init)

    guard !parts.isEmpty,
          parts.allSatisfy({
              !$0.isEmpty && $0 != "." && $0 != ".."
          }) else {
        throw PathError.invalid
    }
    return parts
}

let cases: [(String, Bool)] = [
    ("docs/readme.txt", true),
    ("docs/", true),
    ("report..txt", true),
    ("../outside.txt", false),
    ("docs/../../outside.txt", false),
    ("/tmp/outside.txt", false),
    ("docs//readme.txt", false),
    ("docs/./readme.txt", false),
    ("docs\\..\\outside.txt", false),
    ("C:/outside.txt", false),
    ("docs/\u{0000}bad.txt", false)
]

for (name, expected) in cases {
    let accepted = (try? safeComponents(name)) != nil
    precondition(accepted == expected, "Path policy mismatch")
}
print("Path checks passed")

これは意図的に厳しくした取り込みルールです。スラッシュで区切られた相対パスのみを受け入れ、ディレクトリには末尾のスラッシュを一つだけ許可します。ファイルシステムによる解釈の違いを避けるため、コロンと制御文字も拒否します。業務上これらの文字が本当に必要な場合は、チェック全体を緩めるのではなく、対象を明確にしたテストケースを追加してください。

リンクと重複する書き込み先を確認する

パスが検証を通過しても、エントリの種類を確認する必要があります。リンクを展開してから、そのリンクをたどって次のエントリを書き込んではいけません。また、拡張子を根拠に種類を判断してはいけません。展開ライブラリが公開する種類と属性を読み取り、確実に識別できない種類は拒否します。

書き込み先の各パスを、パス要素単位でインデックス化します。完全に重複するパスだけでなく、「先にファイル docs を書き込み、その後に docs/a.txt を書き込む」という親子関係の競合も検出して拒否します。エントリの順序が逆でも、同様に失敗させる必要があります。そうしないと、ZIP 内の並び順によって結果が変わってしまいます。

大文字・小文字の違いや、Unicode で等価な名前には、別途ポリシーが必要です。取り込むデータでこれらの名前を区別する必要がなければ、各パス要素に Unicode の正規合成とケースフォールディングを適用した値を競合検出に使い、実際の名前は元のまま保持できます。クラウド Mac の現在のボリュームが大文字・小文字をどう扱うかを根拠に、iOS での書き込み結果を推測してはいけません。少なくとも、大文字・小文字が衝突するケースと、結合文字を使ったケースの二種類を用意し、対象のシミュレータで再検証してください。

実際の書き込みで容量制限を検証する

アダプター層は、書き込む前の展開済みデータチャンクを受け取り、累積サイズを確認してからファイルへ書き込む必要があります。整数オーバーフローを避けるため、サイズを直接加算して比較するのではなく、「残りの許容量で足りるか」を比較します。ライブラリがデータ全体をメモリに展開する方式にしか対応せず、途中で制限できない場合は、別のインターフェースを使うか、入力に対して別途厳格な制約を設けてください。

各テスト用 ZIP には、期待する結果を固定しておきます。正常なディレクトリ構成は受け入れ、境界を越えるパス、重複する書き込み先、いずれかの上限を超えるデータは拒否します。さらに、破損した圧縮データを追加し、読み取りエラーが正常終了として扱われないことを確認します。有効な ZIP についても、ライブラリが報告する整合性チェックの結果を確認する必要があります。最終的にファイルが存在するかどうかだけで判断してはいけません。

書き込みに失敗した後は、ステージングディレクトリが削除されたこと、アプリのデータディレクトリが変化していないこと、取り込み先ディレクトリの隣にあらかじめ配置した監視用ファイルの内容が変わっていないことをアサートします。これは、関数がエラーを投げたことだけを確認するより重要です。境界外のファイルに書き込んだ後でエラーが発生している可能性があるためです。

回帰テストの必須チェックに組み込み、境界を維持する

パイプラインでは、まずパス検証スクリプトを実行し、続いて実際の展開ライブラリを呼び出すアプリのテストを実行します。記録するのは、テストケースの識別番号、拒否理由、実際に書き込んだバイト数です。ユーザーファイルの内容は記録しません。テスト用ディレクトリはランダムな名前で作成し、終了時に削除します。システムの一時ディレクトリそのものを展開先のルートにしてはいけません。

事前検証から書き込みまでの間にも、ディレクトリが置き換えられる可能性があります。アプリ専用で、ほかのタスクが変更できないステージングディレクトリを使い、書き込み時にもリンクをたどる処理を拒否してください。デスクトップ上で共有して使うスクリプトなどで、ほかのプロセスがディレクトリを変更できる場合は、文字列のプレフィックスチェックだけではこのリスクを排除できません。

最後に確認するのは三点です。失敗時に取り込み途中のデータが残らないこと、成功時には想定したファイルだけが生成されること、容量制限が実バイト数に基づいて適用されることです。パス検証関数は最初の関門にすぎません。実際の ZIP を使った書き込み結果のアサーションまで行って、初めて検証が完了します。

よくある質問

名前に二つのドットがあれば拒否すればよいですか?

いいえ。独立した .. パス要素、絶対パス、未対応の区切り文字を拒否します。リンク、重複する出力先、書き込み時の境界も確認が必要です。report..txt はそれだけでは不正なパスではありません。

ZIP に記載されたサイズだけで展開量を制限できますか?

できません。記載値は事前確認に使い、書き込み中は実際の展開バイト数を計数します。項目ごとの容量、総容量、項目数に上限を設け、超過時には中断して一時ディレクトリを削除します。

ビルドキューを専用クラウドMacに

物理ノード、機種、利用期間を選ぶだけで、ご自身専用のmacOS環境でXcodeのビルドや自動化タスクを実行できます。実際の利用可能状況は、コンソールに表示される最新情報をご確認ください。

ビルド環境を引き続き確認

機種の仕様と操作ガイドを確認して、次の設定を決めましょう。