アーカイブ展開による任意のファイル書き込み(Tar Slip)

アーカイブ展開による任意のファイル書き込み(Tar Slip)

説明

Tar Slipは、細工されたtarアーカイブ内のパスを使って、展開先のディレクトリー外にファイルを書き込む攻撃です。絶対パスや ../ を含む相対パスにより、重要なシステムファイルや機密データが上書きされるおそれがあります。

想定される影響

  • ファイルシステムの破損: システムファイルが上書きされ、システムが正常に動作しなくなる可能性があります。
  • 情報漏えい: 機密情報が攻撃者に漏れるおそれがあります。
  • 権限昇格: セキュリティに関わるシステムファイルを改変され、権限を奪われる可能性があります。

対処方法

  • 標準の展開フィルターを使用してください。対応するPythonでは extractall(..., filter="data") を明示し、絶対パス、展開先の外を指すリンク、特殊ファイル、過剰な権限を制限してください。
  • 信頼できる親ディレクトリーの下に、専用の非公開ディレクトリーを新しく作成して展開してください。
  • 展開フィルターのない古いランタイムでは、解決後のパスがディレクトリー内に収まることを確認し、シンボリックリンク、ハードリンク、デバイスファイルなどを拒否してください。
  • フィルターだけではアーカイブ爆弾や過剰なファイル数を防げません。合計サイズ、ファイル数、処理時間を別途制限してください。

例

変更前

python
# ランタイムの既定フィルターによるtar展開
import tarfile

def unsafe_extract(tar_file_path, extract_path):
    with tarfile.open(tar_file_path) as tar:
        tar.extractall(path=extract_path)

変更後

python
# 展開先を制限するtar展開
import tarfile
import os
import shutil
from pathlib import Path

def safe_extract(tar_file_path, extract_path):
    destination = Path(extract_path)
    # 既存ファイルやリンクのない専用ディレクトリーにのみ展開
    destination.mkdir(mode=0o700, exist_ok=False)
    destination = destination.resolve()

    with tarfile.open(tar_file_path) as tar:
        if hasattr(tarfile, "data_filter"):
            # 展開フィルターに対応するランタイムの制限付きデータ展開
            tar.extractall(path=destination, filter="data")
            return

        # 古いランタイム用: リンクと特殊ファイルを拒否し、ディレクトリー境界を確認
        for member in tar.getmembers():
            if member.issym() or member.islnk():
                raise ValueError("Tar links are not allowed")
            if not (member.isfile() or member.isdir()):
                raise ValueError("Unsupported tar entry type")

            target = (destination / member.name).resolve()
            try:
                target.relative_to(destination)
            except ValueError as error:
                raise ValueError("Tar entry escapes extraction directory") from error

            if member.isdir():
                target.mkdir(parents=True, exist_ok=True, mode=0o700)
                continue

            target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
            source = tar.extractfile(member)
            if source is None:
                raise ValueError("Tar entry has no file data")
            fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
            with source, os.fdopen(fd, "wb") as output:
                shutil.copyfileobj(source, output)

解説:

  • 変更前: 展開フィルターを明示していません。Python 3.14以降の既定値は data のため、この呼び出しだけで展開先の外への書き込みが許可されるわけではありません。それ以前のバージョンや既定のフィルターを変更した環境では、絶対パスや ../ による書き込みが許可される場合があります。
  • 変更後: 専用ディレクトリーを新しく作成し、利用できる場合は標準の data フィルターを明示します。古いランタイム用の処理では、解決後の各パスを確認し、シンボリックリンク、ハードリンク、特殊ファイルを拒否します。名前に .. があるかどうかだけでは防げない、リンクを使った展開先の逸脱にも対処します。

参考資料