ZIPファイルの展開と制限
展開前の情報と実際の展開量を照合し、拒否した後にファイルが残るか確認します。
説明
ZIPの圧縮サイズだけでは、展開後の容量や保存先を判断できません。各エントリーの名前と展開量を検査し、失敗した途中ファイルを公開しない手順が必要です。
少量の固定ZIPを使い、親ディレクトリへのパス、リンク、展開量を順に変更します。全項目の事前検査とストリーム読取り中の上限を設け、一時領域から完成物だけを公開します。
展開前の情報と実際の展開量を照合し、拒否した後にファイルが残るか確認します。
1圧縮前後のサイズを読む
64バイトの繰返しと5バイトの本文をZIPへ入れます。
from sd08 import *
lab = Extract()
data = archive([("docs/a.txt",b"A"*64),("b.txt",b"hello")])
with zipfile.ZipFile(io.BytesIO(data)) as z:
print([(i.filename,i.compress_size,i.file_size) for i in z.infolist()各エントリーのサイズ情報を読む])compress_sizeは圧縮データ、file_sizeは展開後のサイズです。ZIP全体にはヘッダーもあるため、両者ともZIPファイル全体の長さとは異なります。
2通常項目の後に領域外パスを入れる
前半が正常でも後半に../escape.txtがあれば全体を拒否します。
bad = archive([("ok.txt",b"ok"),("../escape.txt"展開先を外へずらす名前を含める,b"outside")])
print(lab.unpack(bad))
print(lab.files()拒否後の残存ファイルを調べる)path_rejected、ファイル一覧は空です。先頭項目を公開先へ書いてから検査する順序にはしていません。
3リンクを表す属性も検査する
名前が通常でも、UNIX属性でリンクを表すエントリーを用意します。
buf = io.BytesIO()
info = zipfile.ZipInfo("alias")
info.create_system = 3
info.external_attr = (stat.S_IFLNKリンクを示す属性を設定する|0o777)<<16
with zipfile.ZipFile(buf,"w") as z:
z.writestr(info,"../outside.txt")
print(lab.unpack種別検査を含む展開を試す(buf.getvalue()),lab.files())link_rejectedでファイルは残りません。名前と容量だけでなく、展開器が扱う種別も制限します。
4圧縮率が高い入力の展開量を制限する
繰返し129バイトは小さく圧縮されても、上限128バイトを超えます。
large = archive([("repeat.txt",b"A"*129少量の高圧縮率試料を作る)])
with zipfile.ZipFile(io.BytesIO(large)) as z:
i = z.infolist()[0]
print(i.compress_size,i.file_size展開後の必要量を調べる)
print(lab.unpack(large),lab.files())size_rejectedです。圧縮データの量だけで受入可否を決めると、展開後の容量を見落とします。
5合計容量と件数の上限を検査する
1項目が上限内でも、複数項目の合計が超える場合があります。
print(lab.unpack(archive([("a",b"A"*65合計上限を超える組を作る),("b",b"B"*64)])))
print(lab.unpack(archive([(str(i),b"x") for i in range(5)件数上限4を超える])))
print(lab.files())合計129はsize_rejected、5項目はentry_limit。ファイルは空です。容量とファイル数を別々に制限します。
6検査済みの内容だけを公開する
最初の69バイトのZIPを展開します。読取り中も実バイト数を加算し、上限を検査します。
print(lab.unpack(data)実展開量を検査し完成後に公開する)
print(lab.files())
print([(p.name,len(p.read_bytes())実ファイルの長さを照合する) for p in sorted(lab.target.rglob("*.txt"))])publishedとなり、完成した2ファイルだけが公開領域にあります。CRCの検査を含む展開が完了してから、同じ一時親領域内でディレクトリをrenameしています。
7公開済みの一式を上書きしない
別のZIPを同じ公開先へ入れようとします。
print(lab.unpack(archive既存公開先への再展開を試す([("new.txt",b"new")])) )
print((lab.target/"b.txt").read_text())
print(lab.files()既存の一式が維持されたことを調べる)already_publishedで、元のhelloと2ファイルを保ちます。更新する運用では世代別の保存先と切替手順を設けます。
まとめ
名前・種別・件数・展開後の合計量を検査します。
申告サイズに加え、読取り中の実バイト数も制限します。
途中ファイルを公開せず、完成物だけを切り替えます。
操作履歴と解説を振り返れます。
Python 3.15 / 固定の公開試料
from sd08 import *
lab = Extract()
data = archive([("docs/a.txt",b"A"*64),("b.txt",b"hello")])
with zipfile.ZipFile(io.BytesIO(data)) as z:
print([(i.filename,i.compress_size,i.file_size) for i in z.infolist()])[('docs/a.txt', 6, 64), ('b.txt', 7, 5)]bad = archive([("ok.txt",b"ok"),("../escape.txt",b"outside")])
print(lab.unpack(bad))
print(lab.files())path_rejected []
buf = io.BytesIO()
info = zipfile.ZipInfo("alias")
info.create_system = 3
info.external_attr = (stat.S_IFLNK|0o777)<<16
with zipfile.ZipFile(buf,"w") as z:
z.writestr(info,"../outside.txt")
print(lab.unpack(buf.getvalue()),lab.files())link_rejected []
large = archive([("repeat.txt",b"A"*129)])
with zipfile.ZipFile(io.BytesIO(large)) as z:
i = z.infolist()[0]
print(i.compress_size,i.file_size)
print(lab.unpack(large),lab.files())6 129 size_rejected []
print(lab.unpack(archive([("a",b"A"*65),("b",b"B"*64)])))
print(lab.unpack(archive([(str(i),b"x") for i in range(5)])))
print(lab.files())size_rejected entry_limit []
print(lab.unpack(data))
print(lab.files())
print([(p.name,len(p.read_bytes())) for p in sorted(lab.target.rglob("*.txt"))])published
['published/b.txt', 'published/docs/a.txt']
[('b.txt', 5), ('a.txt', 64)]print(lab.unpack(archive([("new.txt",b"new")])) )
print((lab.target/"b.txt").read_text())
print(lab.files())already_published hello ['published/b.txt', 'published/docs/a.txt']
演習完了