2026年10月19日、Pythonのパッケージを作るための定番ツールsetuptoolsの84.1.0と84.2.0が、相次いで公開されました。84.1.0には、アーカイブの展開にまつわる安全面の修正が含まれています。
アーカイブとは、ZIPやtarのように複数のファイルを1つにまとめた形式のことです。修正されたのは、展開したファイルが、指定したフォルダの外に書き出されてしまうおそれのある問題でした。
この種の弱点は、パストラバーサルと呼ばれます。setuptoolsに限らず、ZIPを扱うコードを書くなら誰でも知っておきたい話です。
今回は、何が直ったのかに加えて、自分のコードでZIPを安全に展開する方法も見ていきます。
まずは何が変わったのかを整理しよう¶
最初に、2つのリリースの中身を表にまとめます。内容は、setuptoolsの公式リポジトリにある変更履歴NEWS.rstにもとづいています。
| 版 | 主な中身 |
|---|---|
| 84.1.0 | アーカイブ展開時に、展開先の外へ出る名前を拒否するように |
| 84.1.0 | 古いwheelのインストール経路で、展開先の外にファイルを作れる問題を修正 |
| 84.2.0 | Python 3.15の新しい安定ABI(abi3t)向けの拡張モジュールの作成に対応 |
PyPIの記録によると、84.1.0と84.2.0はどちらも2026年10月19日に公開されています。日本時間では、20日の早朝にあたります。
setuptoolsは、多くの人が直接使わなくても、pipでライブラリを入れるときに裏で動いていることがあるツールです。パッケージ管理の仕組みについては、こちらで紹介しています。【関連記事】pip-auditとは?Pythonライブラリの脆弱性をチェックする方法
パストラバーサルとは何か¶
パストラバーサルは、ファイルの名前やパスに../のような親フォルダを指す書き方を混ぜて、本来の場所の外にあるファイルを読み書きさせる攻撃です。英語のtraversalは、横切る、通り抜けるという意味です。
たとえば、outというフォルダにZIPを展開するつもりでも、中に../escaped.txtという名前のファイルが入っていたらどうなるでしょうか。素直に展開すると、outの1つ上のフォルダにファイルが書き出されてしまいます。
見落としやすいのが、バックスラッシュ¶
ZIPやtarの形式では、フォルダの区切りは/だけと決められています。setuptoolsの変更履歴によると、これまでの守りも、名前を/で区切って..を探す仕組みになっていました。
ところが、..\escaped.txtのようにバックスラッシュを使った名前は、/で区切ると1つの名前に見えてしまいます。それなのにWindowsでは、ファイルシステムがバックスラッシュを区切りとして解釈するため、結果として外に出てしまうおそれがありました。
84.1.0では、バックスラッシュやドライブ名、UNCと呼ばれるネットワーク上のパスを含む名前を、どのOSでも拒否するようになりました。さらに、展開先を計算した結果がフォルダの外になる名前も拒否します。
84.1.0で直った2つの問題¶
変更履歴には、関連する2つの修正が並んでいます。それぞれ、もう少し詳しく見ておきましょう。
アーカイブ展開の修正¶
1つ目は、setuptoolsの中でアーカイブを展開するsetuptools.archive_utilの修正です。先ほどのバックスラッシュの問題に加え、展開先の外に出る名前があると、新しいUnsafeMemberという例外を出して止まるようになりました。
変更履歴によると、これまでは危ない名前のファイルを、黙って飛ばしていたそうです。止まるようにすることで、壊れた、あるいは悪意のあるアーカイブに気づけるようになり、標準ライブラリのtarfileの展開フィルターとも動きがそろったと説明されています。
古いwheelのインストール経路の修正¶
2つ目は、wheelと呼ばれる配布用ファイルを、古いegg形式としてインストールする処理の修正です。GitHubのセキュリティアドバイザリGHSA-xmj3-9gf7-h52wとして、10月19日に公表されました。
アドバイザリによると、細工したwheelの中の設定ファイルを使うと、Windowsで、インストール先の外に__init__.pyというファイルを作れてしまうおそれがありました。深刻度は低で、影響を受けるのは38.2.2以上84.1.0未満です。
アドバイザリには、作られるファイルの中身は攻撃者が決められないこと、任意のコードを実行できるわけではないことも明記されています。また変更履歴によると、この経路は非推奨の古い仕組みからしか通らず、今のsetuptoolsのビルドの仕組みでは通らないとされています。
自分のコードでZIPを安全に展開するには¶
setuptoolsの話は、他人事ではありません。ZIPやtarを展開するコードを自分で書くときにも、同じ注意が必要です。
ZIPの扱い方そのものは、以前の記事で詳しく紹介しました。【関連記事】Pythonのzipfileとは?
危ない名前を見分けるコード¶
ここでは、ZIPの中のファイル名を、展開する前にチェックする方法を試してみます。実験のために、わざと危ない名前を含むZIPを作って確かめました。
import zipfile
from pathlib import Path, PurePosixPath
# わざと危ない名前を含むZIPを作る(実験用)
with zipfile.ZipFile("sample.zip", "w") as zf:
zf.writestr("report/data.csv", "a,b\n1,2\n")
zf.writestr("../escaped.txt", "外に出たい")
zf.writestr("..\\escaped_win.txt", "Windowsで外に出たい")
def is_safe(name, dest):
# バックスラッシュやドライブ指定を含む名前は、どのOSでも拒否する
if "\\" in name or ":" in name:
return False
target = (dest / PurePosixPath(name)).resolve()
return target.is_relative_to(dest.resolve())
dest = Path("out")
with zipfile.ZipFile("sample.zip") as zf:
for name in zf.namelist():
print("OK " if is_safe(name, dest) else "NG ", name)
手元のPython 3.11で実行した結果です。
OK report/data.csv
NG ../escaped.txt
NG ..\escaped_win.txt
普通のファイルだけがOKになり、外に出ようとする2つの名前はNGと判定されました。resolve()で実際の行き先を計算し、is_relative_to()で展開先の中に収まっているかを確かめているのがポイントです。
ちなみに、zipfileのextractall()は、../のような名前から危ない部分を取り除いてから展開する仕組みを持っています。実際に同じZIPをoutではなくoというフォルダにextractall()で展開すると、../escaped.txtはo/escaped.txtとして、フォルダの中に書き出されました。
つまり、標準のzipfileは、黙って安全な場所に書き直してくれるわけです。それでも、展開する前に自分でチェックしておくと、怪しいファイルが混ざっていることにすぐ気づけるのが利点です。
黙って直されると、悪意のあるファイルが届いたこと自体に気づけません。setuptoolsが84.1.0で、飛ばすのをやめて例外で止まるように変えたのも、同じ考え方によるものです。
pathlibでのパスの扱い方は、こちらで解説しています。【関連記事】Pythonのpathlibとは?
tarfileは、新しいPythonなら自動で止めてくれる¶
tarの場合はどうでしょうか。../evil.txtという名前を含むtarを作り、Python 3.15.0のtarfileで、何も指定せずにextractall()を実行してみました。
実際に試したコードはこちらです。メモリ上でtarを作り、そのまま展開しています。
import io
import tarfile
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w") as t:
info = tarfile.TarInfo("../evil.txt")
info.size = 1
t.addfile(info, io.BytesIO(b"x"))
buf.seek(0)
with tarfile.open(fileobj=buf) as t:
try:
t.extractall("outtar")
except tarfile.OutsideDestinationError as e:
print(type(e).__name__)
実行すると、OutsideDestinationErrorという例外が出て、展開先の外への書き出しが止められました。新しいPythonのtarfileは、既定で安全側の展開フィルターを使ってくれるわけです。
ただし、古いPythonではこの守りが既定ではありません。古いバージョンでtarを扱う場合は、extractall()にfilter="data"を明示しておくと安心です。
84.2.0の、Python 3.15向けの新機能¶
84.2.0では、安全面とは別に、Python 3.15向けの新しい機能も加わりました。変更履歴によると、PEP 803で定められたabi3tという、フリースレッド版向けの安定ABIに対応しています。
ABIとは、コンパイルした拡張モジュールとPython本体をつなぐ取り決めのことです。フリースレッド版は、GILと呼ばれる仕組みを外して、複数のスレッドを本当に同時に動かせるようにしたPythonです。安定ABIに対応した拡張モジュールは、Pythonのバージョンが上がっても作り直さずに使えます。
一般の学習者が直接触れることは少ない機能です。それでも、C言語などで書かれた高速なライブラリが、Python 3.15のフリースレッド版に対応しやすくなる土台になります。
学習者と実務の人がやっておきたいこと¶
最後に、今回のニュースから押さえておきたいことを整理します。
| 立場 | やっておきたいこと |
|---|---|
| ライブラリを入れて使うだけの人 | 仮想環境のsetuptoolsを最新版に更新しておく |
| ZIPやtarを展開するコードを書く人 | 展開前に名前をチェックするか、tarではfilter="data"を指定する |
| パッケージを作って配布する人 | 古いsetup_requiresのような仕組みから、今の方法へ移る |
setuptoolsの更新は、pip install --upgrade setuptoolsで行えます。今の版はpip show setuptoolsで確かめられます。仮想環境ごとに入っているので、使っている環境それぞれで確かめてください。
私は10年ほどエンジニアとして開発に関わってきましたが、利用者からアップロードされたZIPを展開する機能のレビューで、パスのチェックが抜けているのを何度も見てきました。外から受け取ったファイルの名前は、そのまま信じないのが、ファイルを扱うコードの基本です。
以前、展開したファイルが想定外のフォルダに散らばり、どこから来たファイルなのか分からなくなって困ったことがあります。展開先をはっきり決め、その外には絶対に書かせないという設計が、結局いちばんの近道でした。
まとめ¶
2026年10月19日、setuptools 84.1.0と84.2.0が公開されました。84.1.0では、アーカイブの展開で指定したフォルダの外にファイルが書き出されるおそれのある問題が直され、84.2.0ではPython 3.15向けの機能が加わっています。
ZIPやtarを扱うコードを書くときは、ファイル名を展開前に確かめる習慣をつけましょう。パストラバーサルの考え方を知っておくだけでも、防げる事故はぐっと増えます。
ここまでお読みいただきありがとうございました。