アーカイブのパストラバーサル

アーカイブのパストラバーサル

説明

libarchiveの archive_entry_pathname()、archive_entry_hardlink()、archive_entry_symlink() などが返すエントリーのパス名やリンク先は、アーカイブに含まれるデータです。そのままファイルシステムAPIに渡すと、絶対パス、.. 要素、リンクによる転送を通じて、意図した展開ディレクトリーの外を指すおそれがあります。

archive_entry_sourcepath() は意味が異なります。アーカイブを作成する際に archive_read_disk が使うローカルファイルシステムの元のパスを返すもので、その値自体がアーカイブに保存されるわけではありません。

想定される影響

  • プロセスの権限でファイルを読み取ったり、ファイルやディレクトリーを作成、置換、名前変更、削除したりする可能性があります。
  • 設定ファイル、実行ファイル、デプロイ先、他のユーザーのデータが改ざんされるおそれがあります。

対処方法

信頼できる固定ディレクトリーを展開先とし、可能ならlibarchiveの次の3つの保護を併用してください。

  • ARCHIVE_EXTRACT_SECURE_NODOTDOT: .. 要素を含むパスを拒否します。
  • ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS: 絶対パスを拒否します。
  • ARCHIVE_EXTRACT_SECURE_SYMLINKS: ディスク上のシンボリックリンクによって展開先が変わるエントリーを拒否します。

libarchive 3.8.9の文書によると、これらの検査は既定では無効です。archive_read_extract() にフラグを渡すか、archive_read_extract2() が使う archive_write_disk オブジェクトに archive_write_disk_set_options() で同じフラグを設定してください。

ARCHIVE_EXTRACT_SAFE_WRITES は、一時ファイルを作成してから名前を変更することで部分的な書き込みを避けるオプションであり、パスを展開先に閉じ込める機能ではありません。既存ファイルの上書きを防ぐ設定も、別の保護です。

独自に展開する場合は、エントリー名と保存されたハードリンク・シンボリックリンクの対象の両方で、対象プラットフォームの絶対パスと .. 要素を拒否してください。事前に開いたディレクトリーを基準に、パスを解決する時点で範囲を制限できるOSのAPIを使ってください。リンクの復元が不要なら、そのエントリーを拒否する方が安全です。文字列の正規化と接頭辞の比較や basename() だけでは、リンク、プラットフォーム固有の区切り文字、ルートパスを安全に扱えません。C++の std::filesystem::path::filename()、lexically_normal()、canonical()、weakly_canonical() も、後でファイルAPIに結果を渡す際のパス制限やリンクの競合防止を単独では保証しません。ディレクトリー構造が不要なら、下の例のように区切り文字を含まない単一のファイル名だけを許可する方が単純です。

例

C

変更前

c
#include <archive_entry.h>
#include <stdio.h>

void extract_unsafe(struct archive_entry *entry) {
    const char *name = archive_entry_pathname(entry);
    FILE *out = fopen(name, "wb");
    if (out != NULL) {
        fclose(out);
    }
}

変更後

c
#include <archive.h>
#include <archive_entry.h>

int extract_entry(struct archive *reader, struct archive_entry *entry) {
    const int flags = ARCHIVE_EXTRACT_SECURE_NODOTDOT |
                      ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS |
                      ARCHIVE_EXTRACT_SECURE_SYMLINKS;

    return archive_read_extract(reader, entry, flags);
}

プロセスの現在の作業ディレクトリーが信頼できる展開ルートであることを前提に、libarchiveのパス検査を有効にします。エラーが返った場合は失敗として扱い、そのエントリーの展開を中止してください。

ディレクトリーのエントリーを扱わない手動展開では、次のような制限を設定できます。

c
#if defined(__APPLE__)
#define _DARWIN_C_SOURCE
#else
#define _POSIX_C_SOURCE 200809L
#endif

#include <archive_entry.h>
#include <fcntl.h>
#include <string.h>

int open_flat_entry(int output_dir_fd, struct archive_entry *entry) {
    const char *name = archive_entry_pathname(entry);
    if (name == NULL || name[0] == '\0' ||
        strchr(name, '/') != NULL || strchr(name, '\\') != NULL ||
        strcmp(name, ".") == 0 || strcmp(name, "..") == 0) {
        return -1;
    }

    return openat(output_dir_fd, name,
                  O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
}

解説:

  • 変更前: アーカイブ内のファイル名を、そのままファイルの作成先に使っています。
  • 変更後: 最初の例は、絶対パス、親ディレクトリー、シンボリックリンクに対する3つの保護を明示的に有効にします。次の例は区切り文字と ./.. を拒否し、安全に開いた output_dir_fd を基準に openat() を呼び出して、既存ファイルと最後のパス要素のシンボリックリンクを拒否します。

C++

変更前

cpp
#include <archive_entry.h>
#include <filesystem>
#include <fstream>

void extract_unsafe_cpp(struct archive_entry *entry) {
    const auto leaf = std::filesystem::path(
        archive_entry_pathname(entry)).filename();
    std::ofstream output{leaf};
}

filename() は最後のパス要素を選ぶだけです。固定の展開ルートを設定したり、既存のシンボリックリンクをたどらずにファイルを開いたりする機能ではありません。

変更後

cpp
#include <archive.h>
#include <archive_entry.h>

int extract_entry_cpp(struct archive *reader, struct archive_entry *entry) {
    constexpr int flags = ARCHIVE_EXTRACT_SECURE_NODOTDOT |
                          ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS |
                          ARCHIVE_EXTRACT_SECURE_SYMLINKS;

    return archive_read_extract(reader, entry, flags);
}

この例も、現在の作業ディレクトリーが信頼できる展開ルートであることを前提にしています。libarchiveが返すエラーは失敗として処理してください。

参考資料