一覧SD-08-3 · ファイル・パス・アップロード

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 / 固定の公開試料

1
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)]
2
bad = archive([("ok.txt",b"ok"),("../escape.txt",b"outside")]) print(lab.unpack(bad)) print(lab.files())
path_rejected
[]
3
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
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 []
5
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
[]
6
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)]
7
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']

演習完了