web-dev-qa-db-ja.com

Python)を使用してZipまたはtarを安全に抽出します

ユーザーが送信したZipファイルとtarファイルをディレクトリに抽出しようとしています。 zipfileの extractall メソッド(tarfileの extractall と同様)のドキュメントには、パスが絶対パスであるか、宛先パスの外側にある..パスが含まれている可能性があると記載されています。代わりに、次のように自分でextractを使用できます。

some_path = '/destination/path'
some_Zip = '/some/file.Zip'
zipf = zipfile.ZipFile(some_Zip, mode='r')
for subfile in zipf.namelist():
    zipf.extract(subfile, some_path)

これは安全ですか?この場合、アーカイブ内のファイルがsome_pathの外に収まる可能性はありますか?もしそうなら、ファイルが宛先ディレクトリの外に出ないようにするにはどうすればよいですか?

26
jterrace

注:python 2.7.4以降、これはZipアーカイブでは問題になりません。詳細は下部にありますこの回答はタールアーカイブに焦点を当てています。

パスが実際に指している場所を特定するには、os.path.abspath()を使用します(ただし、パスコンポーネントとしてのシンボリックリンクに関する警告に注意してください)。 zipファイルからのパスをabspathで正規化し、現在のディレクトリをプレフィックスとして含まない場合、そのパスはその外側を指しています。

ただし、アーカイブから抽出されたシンボリックリンクのvalueも確認する必要があります(tarfileとunix zipfileの両方でシンボリックリンクを格納できます)。これは、システムライブラリに自分自身をインストールするだけのアプリケーションではなく、意図的にセキュリティをバイパスすることわざの「悪意のあるユーザー」が心配な場合に重要です。

これは前述の警告です。サンドボックスにディレクトリを指すシンボリックリンクがすでに含まれている場合、abspathは誤解されます。サンドボックス内を指すシンボリックリンクでさえ危険な場合があります。シンボリックリンクsandbox/subdir/foo -> ..sandboxを指しているため、パスsandbox/subdir/foo/../.bashrcは許可しないでください。これを行う最も簡単な方法は、前のファイルが抽出されるまで待って、os.path.realpath()を使用することです。幸い、extractall()はジェネレーターを受け入れるので、これは簡単に実行できます。

コードを要求するので、ここにアルゴリズムを説明するビットがあります。サンドボックスの外側の場所へのファイルの抽出(要求されたもの)だけでなく、サンドボックスの外側の場所を指すリンクのサンドボックス内の作成も禁止します。誰かがそれを超えて漂遊ファイルやリンクをこっそり盗むことができるかどうか知りたいです。

import tarfile
from os.path import abspath, realpath, dirname, join as joinpath
from sys import stderr

resolved = lambda x: realpath(abspath(x))

def badpath(path, base):
    # joinpath will ignore base if path is absolute
    return not resolved(joinpath(base,path)).startswith(base)

def badlink(info, base):
    # Links are interpreted relative to the directory containing the link
    tip = resolved(joinpath(base, dirname(info.name)))
    return badpath(info.linkname, base=tip)

def safemembers(members):
    base = resolved(".")

    for finfo in members:
        if badpath(finfo.name, base):
            print >>stderr, finfo.name, "is blocked (illegal path)"
        Elif finfo.issym() and badlink(finfo,base):
            print >>stderr, finfo.name, "is blocked: Hard link to", finfo.linkname
        Elif finfo.islnk() and badlink(finfo,base):
            print >>stderr, finfo.name, "is blocked: Symlink to", finfo.linkname
        else:
            yield finfo

ar = tarfile.open("testtar.tar")
ar.extractall(path="./sandbox", members=safemembers(ar))
ar.close()

編集:python 2.7.4から、これはZipアーカイブでは問題になりません:メソッド- zipfile.extract() サンドボックス外でのファイルの作成を禁止します:

注:メンバーのファイル名が絶対パスの場合、ドライブ/ UNC共有ポイントと先頭(バック)スラッシュは削除されます。例:///foo/barfoo/barになります。 Unix、およびC:\foo\barはWindowsではfoo\barになります。また、メンバーファイル名のすべての".."コンポーネントが削除されます。例:../../foo../../ba..rfoo../ba..rになります。 Windowsでは、不正な文字(:<>|"?、および*)はアンダースコア(_)に置き換えられます。

tarfileクラスは同様にサニタイズされていないため、上記の回答は引き続き適用されます。

38
alexis

ZipFile.infolist()/TarFile.next()/TarFile.getmembers()を使用して、アーカイブ内の各エントリに関する情報を取得し、パスを正規化し、ファイルを自分で開き、ZipFile.open()/TarFile.extractfile()を使用します。エントリのファイルのようなものを取得し、エントリデータを自分でコピーします。

Zipファイルを空のディレクトリにコピーします。次に、os.chrootを使用して、そのディレクトリをルートディレクトリにします。次に、そこで解凍します。

または、ディレクトリを無視する-jフラグを使用してunzip自体を呼び出すこともできます。

import subprocess
filename = '/some/file.Zip'
rv = subprocess.call(['unzip', '-j', filename])
2
Roland Smith

一般的な回答とは異なり、ファイルを安全に解凍することは、Python 2.7.4の時点では完全には解決されていません。extractallメソッドは依然として危険であり、直接またはシンボリックの解凍を通じてパストラバーサルにつながる可能性があります。リンク。これが私の最終的な解決策であり、抽出メソッドが脆弱であったPython 2.7.4より前のバージョンも含めて、Pythonのすべてのバージョンで両方の攻撃を防ぐ必要があります。

import zipfile, os

def safe_unzip(Zip_file, extractpath='.'):
    with zipfile.ZipFile(Zip_file, 'r') as zf:
        for member in zf.infolist():
            abspath = os.path.abspath(os.path.join(extractpath, member.filename))
            if abspath.startswith(os.path.abspath(extractpath)):
                zf.extract(member, extractpath)

編集済み:変数名の衝突を修正しました。 JuusoOhtonenに感謝します。

2
shellster