[PHP]zip_entry_nameとは?非推奨のZIPエントリ名取得関数とZipArchiveへの移行を徹底解説

PHP

はじめに

これまでの記事で、非推奨の手続き型ZIP関数群から zip_close()、zip_entry_close()、zip_entry_compressedsize()、zip_entry_compressionmethod()、zip_entry_filesize() を解説してきました。今回取り上げる zip_entry_name() も同じ関数群に属し、ZIPアーカイブ内の各エントリの**名前(アーカイブ内でのパスを含むファイル名)**を取得するための関数です。

これまで紹介してきたサイズや圧縮方式といった属性情報と並んで、エントリ名は最も基本的かつ使用頻度の高い情報です。「アーカイブの中にどんなファイルが入っているか」を知りたい場面では、必ずこの情報が必要になります。前回までと同様、この関数もPHP 7.2.0以降で非推奨とされており、現在は ZipArchive クラスの利用が推奨されています。本記事では、この関数の役割と現代的な代替実装、そしてエントリ名を扱う際のセキュリティ上の注意点まで詳しく解説します。


関数概要

項目内容
関数名zip_entry_name()
所属拡張Zip拡張(手続き型インターフェース)
シグネチャzip_entry_name(resource $zip_entry): string
引数$zip_entry — zip_read() で取得したエントリのリソース
戻り値エントリの名前(アーカイブ内のパスを含む)
対応バージョンPHP 4.1.0以降。PHP 7.2.0以降は非推奨
推奨される代替ZipArchive::getNameIndex() または statIndex() の 'name' フィールド
セキュリティ関連「Zip Slip」脆弱性への対策が必要

エントリ名の構造(イメージ図)

  ZIPアーカイブ "project.zip" の内部
  ┌─────────────────────────────────┐
  │ src/                              │ ← ディレクトリエントリ(末尾が"/")
  │ src/index.php                     │ ← パスを含むファイル名
  │ src/lib/helper.php                │ ← ネストしたパス
  │ README.md                         │ ← ルート直下のファイル
  └─────────────────────────────────┘
              │
              ▼
  zip_entry_name($entry)
              │
              ▼
  "src/lib/helper.php" のような
  アーカイブ内での相対パス文字列が返る
  ★この記事の対象

  【注意】悪意あるアーカイブでは...
  "../../etc/passwd" のような
  ディレクトリトラバーサルを狙った名前が含まれる可能性がある

ポイントは、エントリ名が単なるファイル名ではなく、アーカイブ内のディレクトリ構造を含む相対パスであるという点です。そしてこの性質こそが、後述する「Zip Slip」と呼ばれるセキュリティ上の脆弱性の原因にもなります。


実践サンプル7選

例1:非推奨の古い関数を使った基本的な使い方(参考・非推奨)

<?php

class LegacyEntryNameReader
{
    /**
     * 注意: PHP 7.2.0以降、この一連の関数は非推奨です
     * 新規開発ではZipArchiveの使用を強く推奨します
     */
    public function listNames(string $zipPath): array
    {
        $names = [];
        $zip = zip_open($zipPath);

        if (is_resource($zip)) {
            while ($entry = zip_read($zip)) {
                $names[] = zip_entry_name($entry);
            }
            zip_close($zip);
        }

        return $names;
    }
}

$reader = new LegacyEntryNameReader();
print_r($reader->listNames('/tmp/sample.zip'));

例2:ZipArchiveを使った推奨の代替実装

<?php

class ModernEntryNameReader
{
    /**
     * ZipArchive::getNameIndex()でインデックス指定により
     * エントリ名を直接取得できる
     */
    public function listNames(string $zipPath): array
    {
        $names = [];
        $zip = new ZipArchive();

        if ($zip->open($zipPath) === true) {
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $names[] = $zip->getNameIndex($i);
            }
            $zip->close();
        }

        return $names;
    }
}

$reader = new ModernEntryNameReader();
print_r($reader->listNames('/tmp/sample.zip'));

例3:Zip Slip脆弱性を防ぐ安全な展開クラス

<?php

class SafeZipExtractor
{
    /**
     * エントリ名に"../"などが含まれる悪意あるアーカイブ(Zip Slip攻撃)から
     * 展開先ディレクトリの外にファイルが書き出されるのを防ぐ
     */
    public function extractSafely(string $zipPath, string $destDir): array
    {
        $extracted = [];
        $zip = new ZipArchive();

        if ($zip->open($zipPath) !== true) {
            throw new RuntimeException('ZIPファイルを開けませんでした');
        }

        $realDest = realpath($destDir);

        for ($i = 0; $i < $zip->numFiles; $i++) {
            $name = $zip->getNameIndex($i);

            // 危険なパスパターンを事前に拒否する
            if (str_contains($name, '..') || str_starts_with($name, '/')) {
                continue;
            }

            $targetPath = $realDest . DIRECTORY_SEPARATOR . $name;
            $zip->extractTo($realDest, $name);
            $extracted[] = $name;
        }

        $zip->close();
        return $extracted;
    }
}

$extractor = new SafeZipExtractor();
// print_r($extractor->extractSafely('/tmp/sample.zip', '/tmp/extract_dir'));

例4:特定の拡張子のエントリだけを抽出するフィルター

<?php

class ExtensionFilter
{
    /**
     * エントリ名の拡張子を調べ、
     * 特定の種類のファイルだけを抽出する
     */
    public function filterByExtension(string $zipPath, array $allowedExtensions): array
    {
        $matched = [];
        $zip = new ZipArchive();

        if ($zip->open($zipPath) === true) {
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $name = $zip->getNameIndex($i);
                $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));

                if (in_array($ext, $allowedExtensions, true)) {
                    $matched[] = $name;
                }
            }
            $zip->close();
        }

        return $matched;
    }
}

$filter = new ExtensionFilter();
print_r($filter->filterByExtension('/tmp/sample.zip', ['php', 'js']));

例5:ディレクトリ構造をツリー形式で可視化するツール

<?php

class ArchiveTreeVisualizer
{
    /**
     * エントリ名のパス構造を解析し、
     * 階層的なツリー表示を生成する
     */
    public function buildTree(string $zipPath): array
    {
        $tree = [];
        $zip = new ZipArchive();

        if ($zip->open($zipPath) === true) {
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $name = $zip->getNameIndex($i);
                $parts = explode('/', rtrim($name, '/'));

                $current = &$tree;
                foreach ($parts as $part) {
                    if (!isset($current[$part])) {
                        $current[$part] = [];
                    }
                    $current = &$current[$part];
                }
                unset($current);
            }
            $zip->close();
        }

        return $tree;
    }
}

$visualizer = new ArchiveTreeVisualizer();
print_r($visualizer->buildTree('/tmp/sample.zip'));

例6:エントリ名の文字エンコーディング問題に対処するクラス

<?php

class EncodingAwareNameReader
{
    /**
     * Windowsで作成されたZIPファイルのエントリ名が
     * Shift_JISになっている場合などに対応する
     */
    public function readNamesWithEncoding(string $zipPath, string $sourceEncoding = 'CP932'): array
    {
        $names = [];
        $zip = new ZipArchive();

        if ($zip->open($zipPath) === true) {
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $rawName = $zip->getNameIndex($i);

                // UTF-8として妥当でない場合は変換を試みる
                if (!mb_check_encoding($rawName, 'UTF-8')) {
                    $rawName = mb_convert_encoding($rawName, 'UTF-8', $sourceEncoding);
                }

                $names[] = $rawName;
            }
            $zip->close();
        }

        return $names;
    }
}

$reader = new EncodingAwareNameReader();
print_r($reader->readNamesWithEncoding('/tmp/japanese_names.zip'));

例7:エントリ名を使って必要なファイルの存在を検証するクラス

<?php

class RequiredFileValidator
{
    /**
     * アップロードされたZIPに必須のファイルが
     * 含まれているかをエントリ名で検証する
     */
    public function validate(string $zipPath, array $requiredFiles): array
    {
        $zip = new ZipArchive();

        if ($zip->open($zipPath) !== true) {
            throw new RuntimeException('ZIPファイルを開けませんでした');
        }

        $existingNames = [];
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $existingNames[] = $zip->getNameIndex($i);
        }
        $zip->close();

        $missing = array_diff($requiredFiles, $existingNames);

        return [
            'valid'   => empty($missing),
            'missing' => array_values($missing),
        ];
    }
}

$validator = new RequiredFileValidator();
print_r($validator->validate('/tmp/plugin.zip', ['plugin.php', 'readme.txt']));

関連関数との比較

関数/メソッド役割zip_entry_nameとの違い
zip_entry_name()手続き型でエントリ名を取得本記事の対象。PHP 7.2.0以降は非推奨
ZipArchive::getNameIndex()インデックス指定でエントリ名を取得推奨される代替。インデックスによる直接アクセスが可能
ZipArchive::statIndex()エントリの統計情報を取得nameを含む複数の情報を一括取得できる
ZipArchive::locateName()エントリ名からインデックスを検索名前からインデックスを逆引きする、対になる機能
ZipArchive::extractTo()エントリをファイルシステムに展開名前を指定して選択的に展開することも可能

よくある落とし穴(注意点)

  1. Zip Slip脆弱性への対策が必須である 最も重要な注意点です。エントリ名に ../../etc/passwd のようなパスが含まれる悪意あるZIPファイルを無検証で展開すると、展開先ディレクトリの外にファイルが書き出され、システムファイルを上書きされる危険があります。外部からアップロードされたZIPを扱う場合は、必ずエントリ名を検証してから展開しましょう(例3を参照)。
  2. PHP 7.2.0以降でE_DEPRECATED警告が発生する これまでの記事と同様、この関数群を使用すると非推奨警告が発生します。早期の ZipArchive への移行が推奨されます。
  3. 日本語ファイル名の文字化け問題 Windowsの標準機能で作成されたZIPファイルでは、エントリ名がCP932(Shift_JIS)でエンコードされている場合があります。UTF-8環境でそのまま扱うと文字化けするため、必要に応じてエンコーディング変換が必要です(例6を参照)。
  4. ディレクトリエントリとファイルエントリの区別 ディレクトリを表すエントリは、名前の末尾が / になっているのが一般的です。ファイルのみを処理したい場合は、この特徴を使った判定が必要になります。
  5. 同名エントリが複数存在する可能性 ZIP形式の仕様上、同じ名前のエントリが複数含まれるアーカイブを作成することも技術的には可能です。エントリ名を連想配列のキーとして使う場合、意図せず上書きされる可能性がある点に留意しましょう。

まとめ

観点まとめ
何をする関数かZIPエントリの名前(アーカイブ内の相対パス)を取得する(手続き型インターフェース)
現在の状態PHP 7.2.0以降で非推奨。将来的に削除される可能性がある
推奨される代替ZipArchive::getNameIndex() または statIndex() の name フィールド
最重要のセキュリティ注意点Zip Slip脆弱性 — エントリ名の検証なしに展開してはいけない
その他の注意点日本語ファイル名のエンコーディング、ディレクトリエントリの識別

zip_entry_name() は、ZIPアーカイブの中身を把握するための最も基本的な情報を提供する関数です。ZipArchive への移行が推奨されるのはもちろんですが、それ以上に重要なのが、エントリ名を扱う際のZip Slip脆弱性への対策です。外部から受け取ったZIPファイルを展開する処理を実装する際は、必ずエントリ名の検証を組み込むようにしましょう。

タイトルとURLをコピーしました