2012-11-29 12 views
18

これは質問about zip bombsに関連していますが、gzipまたはbzip2圧縮を念頭に置いてください。 .tar.gzファイルを受け付けるWebサービス。gzipまたはbzip2の爆弾から身を守るには?

Pythonは使いやすい便利なものですが、zipbombに対する保護機能はありません。

tarfileモジュールを使用したPythonコードでは、zarファイルモジュールからの余分なロジック(例:透過的な圧縮解除サポート)を重複しないで、ジップボムを検出する最もエレガントな方法は何でしょうか?

さらに、単純ではありません。実際のファイルは含まれていません。入力はファイルのようなオブジェクトです(Webフレームワークによって提供され、ユーザーがアップロードしたファイルを表します)。

+0

TarInfo.sizeを使用できませんか? – fatfredyy

+1

@fatfredyyあなたはタールを解凍する前にgz爆弾を打つことができます。 – Jakozaur

+1

あなたは爆弾の効果を心配していますか?メモリ使用量のみ?抽出されたディスクスペースの使用量(参照された質問ごと) –

答えて

2

私は答えが次のようだと思います。簡単な、レディメイドのソリューションはありません。ここで私は今使用しているものである:

class SafeUncompressor(object): 
    """Small proxy class that enables external file object 
    support for uncompressed, bzip2 and gzip files. Works transparently, and 
    supports a maximum size to avoid zipbombs. 
    """ 
    blocksize = 16 * 1024 

    class FileTooLarge(Exception): 
     pass 

    def __init__(self, fileobj, maxsize=10*1024*1024): 
     self.fileobj = fileobj 
     self.name = getattr(self.fileobj, "name", None) 
     self.maxsize = maxsize 
     self.init() 

    def init(self): 
     import bz2 
     import gzip 
     self.pos = 0 
     self.fileobj.seek(0) 
     self.buf = "" 
     self.format = "plain" 

     magic = self.fileobj.read(2) 
     if magic == '\037\213': 
      self.format = "gzip" 
      self.gzipobj = gzip.GzipFile(fileobj = self.fileobj, mode = 'r') 
     elif magic == 'BZ': 
      raise IOError, "bzip2 support in SafeUncompressor disabled, as self.bz2obj.decompress is not safe" 
      self.format = "bz2" 
      self.bz2obj = bz2.BZ2Decompressor() 
     self.fileobj.seek(0) 


    def read(self, size): 
     b = [self.buf] 
     x = len(self.buf) 
     while x < size: 
      if self.format == 'gzip': 
       data = self.gzipobj.read(self.blocksize) 
       if not data: 
        break 
      elif self.format == 'bz2': 
       raw = self.fileobj.read(self.blocksize) 
       if not raw: 
        break 
       # this can already bomb here, to some extend. 
       # so disable bzip support until resolved. 
       # Also monitor http://stackoverflow.com/questions/13622706/how-to-protect-myself-from-a-gzip-or-bzip2-bomb for ideas 
       data = self.bz2obj.decompress(raw) 
      else: 
       data = self.fileobj.read(self.blocksize) 
       if not data: 
        break 
      b.append(data) 
      x += len(data) 

      if self.pos + x > self.maxsize: 
       self.buf = "" 
       self.pos = 0 
       raise SafeUncompressor.FileTooLarge, "Compressed file too large" 
     self.buf = "".join(b) 

     buf = self.buf[:size] 
     self.buf = self.buf[size:] 
     self.pos += len(buf) 
     return buf 

    def seek(self, pos, whence=0): 
     if whence != 0: 
      raise IOError, "SafeUncompressor only supports whence=0" 
     if pos < self.pos: 
      self.init() 
     self.read(pos - self.pos) 

    def tell(self): 
     return self.pos 

コードの一部が無効になっているように、それは、bzip2のためにうまく機能しません。その理由は、bz2.BZ2Decompressor.decompressがすでに望ましくない大きなチャンクを生成している可能性があるためです。限られたメモリを使用しながら、

3

Linux用に開発する場合は、別々のプロセスで解凍を実行し、ulimitを使用してメモリ使用量を制限することができます。

import subprocess 
subprocess.Popen("ulimit -v %d; ./decompression_script.py %s" % (LIMIT, FILE)) 

decompression_script.pyは、ディスクに書き込む前にメモリ内のファイル全体を解凍する必要があります。

+0

これはうまくいくかもしれませんが、これはエレガントなものではありません。また、データをスクリプトにパイプする必要があり、ulimit呼び出しと組み合わせるのが少し難しくなります。 –

+0

ファイルを作成するだけで、パイプを必要とせず、その場合にコンパイルすることができます。 – Jakozaur

+0

しかし、私はメモリにデータを持っています。なぜここでファイルを扱わなければならないのですか? –

5

これは、GZIPストリームの非圧縮サイズを決定します:

#!/usr/bin/python 
import sys 
import zlib 
f = open(sys.argv[1], "rb") 
z = zlib.decompressobj(15+16) 
total = 0 
while True: 
    buf = z.unconsumed_tail 
    if buf == "": 
     buf = f.read(1024) 
     if buf == "": 
      break 
    got = z.decompress(buf, 4096) 
    if got == "": 
     break 
    total += len(got) 
print total 
if z.unused_data != "" or f.read(1024) != "": 
    print "warning: more input after end of gzip stream" 

これは、抽出されたときでtarファイル内のすべてのファイルのために必要なスペースのわずかな過大評価を返します。長さには、これらのファイル、およびtarディレクトリー情報が含まれます。

gzip.pyコードは、入力データのサイズを除いて、圧縮解除されたデータの量を制御しません。 gzip.pyでは、一度に1024圧縮バイトを読み込みます。圧縮されていないデータ(1032 * 1024、ここで1032:1はdeflateの圧縮率の最大値)に対して最大約1056768バイトのメモリ使用量でOKならgzip.pyを使用できます。ここでの解法は、圧縮されていないデータの量を制限する第2引数を使用してzlib.decompressを使用します。 gzip.pyはそうではありません。

これは正確にtar形式をデコードすることにより抽出されたタールエントリの合計サイズを決定します:

#!/usr/bin/python 

import sys 
import zlib 

def decompn(f, z, n): 
    """Return n uncompressed bytes, or fewer if at the end of the compressed 
     stream. This only decompresses as much as necessary, in order to 
     avoid excessive memory usage for highly compressed input. 
    """ 
    blk = "" 
    while len(blk) < n: 
     buf = z.unconsumed_tail 
     if buf == "": 
      buf = f.read(1024) 
     got = z.decompress(buf, n - len(blk)) 
     blk += got 
     if got == "": 
      break 
    return blk 

f = open(sys.argv[1], "rb") 
z = zlib.decompressobj(15+16) 
total = 0 
left = 0 
while True: 
    blk = decompn(f, z, 512) 
    if len(blk) < 512: 
     break 
    if left == 0: 
     if blk == "\0"*512: 
      continue 
     if blk[156] in ["1", "2", "3", "4", "5", "6"]: 
      continue 
     if blk[124] == 0x80: 
      size = 0 
      for i in range(125, 136): 
       size <<= 8 
       size += blk[i] 
     else: 
      size = int(blk[124:136].split()[0].split("\0")[0], 8) 
     if blk[156] not in ["x", "g", "X", "L", "K"]: 
       total += size 
     left = (size + 511) // 512 
    else: 
     left -= 1 
print total 
if blk != "": 
    print "warning: partial final block" 
if left != 0: 
    print "warning: tar file ended in the middle of an entry" 
if z.unused_data != "" or f.read(1024) != "": 
    print "warning: more input after end of gzip stream" 

あなたは爆弾のためのtarファイルをスキャンするこのバリアントを使用することができます。これには、ヘッダ情報のサイズを大きくして、そのデータを圧縮解除する必要があるという利点があります。

.tar.bz2アーカイブの場合、Python bz2ライブラリ(少なくとも3.3以降)は、あまりにも多くのメモリを消費するbz2爆弾にとって不可避的に安全ではありません。 bz2.decompress関数は、zlib.decompressのように2番目の引数を提供しません。これは、ランレングスコーディングにより、bz2フォーマットがzlibよりもはるかに高い最大圧縮率を持つという事実によって、さらに悪化しています。 bzip2は、1 GBのゼロを722バイトに圧縮します。したがって、のように、入力を2番目の引数なしで計測することによって、bz2.decompressの出力を計測することはできません。解凍された出力サイズに制限がないことは、Pythonインターフェイスの基本的な欠陥です。

3.3の_bz2module.cを調べて、この問題を避けるために文書化されていない方法があるかどうかを確認しました。その周りに道はない。 decompress関数は、提供されたすべての入力を解凍できるまで、結果バッファを増やし続けます。 _bz2module.cを修正する必要があります。

+0

これは本当に効果的ですか? tarは、その周りにgzipラッパーを解凍しないで、サイズについてどのように知っていますか?私はgzipの爆弾ではなく、タールの爆弾を心配しています! –

+0

私はそれをテストしましたが、うまくいきません: '.tar.gz'ファイルにゼロの10GBファイルをパックすると、10MBの大きなファイルになります。 'ulimit -v 200000'を設定してそのファイルでコードを実行すると失敗しますので、10MB以上の入力を使用するため、zipbombsの影響を受けます。 –

+0

奇数さて、上記を試してみてください。 –

9

resource moduleを使用すると、プロセスとその子プロセスが使用できるリソースを制限できます。あなたは、メモリ内に解凍する必要がある場合

その後自動的に以前の値に復元するコンテキストマネージャを使用して、例えばresource.RLIMIT_AS(又はRLIMIT_DATARLIMIT_STACK)を設定できます。

import contextlib 
import resource 

@contextlib.contextmanager 
def limit(limit, type=resource.RLIMIT_AS): 
    soft_limit, hard_limit = resource.getrlimit(type) 
    resource.setrlimit(type, (limit, hard_limit)) # set soft limit 
    try: 
     yield 
    finally: 
     resource.setrlimit(type, (soft_limit, hard_limit)) # restore 

with limit(1 << 30): # 1GB 
    # do the thing that might try to consume all memory 

限界に達した場合。 MemoryErrorが発生します。

+0

アーカイブのサイズや圧縮されていないデータの量に関係なく、適切に実装されたtar.gz抽出プログラムが約40K以上のメモリを占める理由はありません。抽出されたときのディスク容量は別の問題ですが、これは役に立たないでしょう。 –

+0

@マークアドラー:すべてのデータがメモリに保存されている場合のみOPに関心があります:*「実際のファイルはありません」*、「メモリにデータがあります」* – jfs

+1

ポスターでリンクされた質問には、 _ "として発行すると、開かれたときにサーバーのディスクがいっぱいになります。"だからそれは明らかではない。 –

関連する問題