Python を 3.14 に上げたら、これまで通っていた tar 展開が落ちます。
tarfile.OutsideDestinationError: '../escape' would be extracted to '/tmp/escape', which is outside the destination
コピペ検索用の表記ゆれです(どちらも実測)。展開先の外を指すメンバーか、外を指すシンボリックリンクかで、例外の名前が変わります。
tarfile.OutsideDestinationError: ... would be extracted to ... which is outside the destinationtarfile.LinkOutsideDestinationError: ... would link to ... which is outside the destination
どちらも Python 3.14 で新設された例外です。 3.13 以前には存在しません。
3.13 では、この展開は何をしていたのか
同じコードは 3.13 まで終了コード 0 で通ります。../escape というメンバーを含む tar を展開すると、次のことが起きます。
import tarfile
with tarfile.open("archive.tar") as tar:
tar.extractall("output") # archive.tar に ../escape が入っている
../escape が output の外へ実際に書き出されます。 展開先の親ディレクトリにファイルができます(実測:3.13 で展開先の外に escape が生成されることを確認)。
これはディレクトリ・トラバーサルです。悪意のある tar が ../../etc/cron.d/... のようなメンバーを含んでいれば、展開しただけで展開先の外が書き換わります。
Python 3.14 は、既定の展開フィルタを data に変えました。 data フィルタは、展開先の外へ出るメンバーを展開する前に拒否します。3.14 のエラーは、3.13 まで黙って許していたトラバーサルを止めています。
直し方
アーカイブを作っている側を直します。 メンバー名を展開先に収まる相対パスにします。
# 悪い:展開先の外を指す
member = tarfile.TarInfo("../escape")
# 良い:展開先に収まる
member = tarfile.TarInfo("escape")
自分がアーカイブの作成者なら、これで両側が揃います。
受け取った tar を展開する側で、正当なメンバーまで拒否されて困る場合は、まずそのメンバーが本当に展開先の外を指しているのかを確認してください。3.14 が拒否するのは、展開したら output の外に出るメンバーだけです。正当なアーカイブなら、そういうメンバーは含まれていないはずです。
filter='fully_trusted' は、脆弱性をそのまま元に戻します
検索で最初に出てくる回避策が、filter='fully_trusted' を渡して 3.13 の挙動に戻す方法です。
tar.extractall("output", filter='fully_trusted') # エラーは消える
エラーは消えますが、../escape はまた展開先の外へ書き出されます(実測:3.14 でも fully_trusted を指定すると外に escape が生成される)。/tmp/... のような絶対パスのメンバーも、外を指すシンボリックリンクも同様です。
fully_trusted は「このアーカイブは完全に信頼できる」という宣言です。アーカイブの内容を完全に信頼できる、または署名検証などで内容を同等に検証している場合にだけ使うものです。受け取った tar・ダウンロードした tar にエラー抑止のために付けると、3.14 が塞いだ穴を自分で開け直すことになります。
エラーを消したいだけなら、fully_trusted は使わないでください。 正しい対処はアーカイブ側を直すことで、それができないなら、そのメンバーは本当に受け入れてよいのかを疑う場面です。
どのフィルタが何を止めるのか
展開先の外へ出るメンバーは、../escape のような相対パス、/tmp/... のような絶対パス、外を指すシンボリックリンクの 3 通りあります。Python 3.14 で、フィルタ別に測った結果です(実測)。
| メンバー | data(3.14 の既定) | tar | fully_trusted |
|---|---|---|---|
../escape(相対) | 拒否 | 拒否 | 外へ書き出す |
/tmp/abs-escape(絶対) | 展開先内に収める | 展開先内に収める | 外へ書き出す |
| 外を指す symlink | 拒否 | 作成する | 作成する |
読み方は 2 つです。
data(既定)は 3 通りすべてを止めます。 3.14 の既定がdataになったことが、この境界の実体です。fully_trustedは 3 通りすべてを素通しします。 相対パスも絶対パスも、そして symlink も、展開先の外へ出ます。
tar は中間で、相対パスと絶対パスのトラバーサルは止めますが、外を指す symlink は止めません(作成します)。data と tar の差はここです。
絶対パスも危険です。 data と tar が先頭の / を落として展開先内に収めるので安全に見えますが、fully_trusted を指定すると /etc/... のような絶対パスがそのまま外の実パスへ書き込まれます(実測)。3.13 の既定も同じく外へ書きました。
予告は 3.12 から出ています
このエラーには予告があります。3.12 でも 3.13 でも、フィルタを指定せずに展開すると次が出ます。
DeprecationWarning: Python 3.14 will, by default, filter extracted tar archives and reject files or modify their metadata. Use the filter argument to control this behavior.
落ちる版(3.14)を名指ししています。 ただし DeprecationWarning なので、既定のフィルタでは表示されません。展開処理を書いているコードがパッケージ側のモジュールにあると、-W always::DeprecationWarning を付けない限り出ません。
3.14 に上げる前に落ちる箇所を洗い出したいなら、フィルタを明示して走らせるのが確実です。3.12・3.13 でも filter='data' を渡せば、3.14 と同じ OutsideDestinationError が出ます(実測)。展開している全箇所に filter='data' を明示しておけば、既定値の変更に左右されなくなります。
切り分け
- 自分で作った tar なのに落ちる → メンバー名に
..が入っていないか確認してください。エラーメッセージの'...' would be extracted toに実際のメンバー名が出ます。 OutsideDestinationErrorではなくLinkOutsideDestinationError→ メンバーではなく、展開先の外を指すシンボリックリンクです。原因は同じ(3.14 の既定フィルタ)ですが、リンク先を直す必要があります。filter='fully_trusted'でエラーが消えた → トラバーサルは許したままです。上の節のとおり、fully_trustedは内容を完全に信頼できる(または同等に検証している)アーカイブ用で、エラー抑止用ではありません。- 絶対パス(
/etc/...)のメンバーを心配している →data(既定)とtarは展開先内に収めますが、fully_trustedは絶対パスをそのまま外の実パスへ書きます(実測)。..と同じく危険です。安全なのは既定のdataを使うことです。 - 他の Python 3.14 のエラーを踏んだ → 同じ版境界の姉妹記事があります。configparser の InvalidWriteError は、同じく「3.13 まで危険な動作を黙って通していた」ものを 3.14 が止めた例です。
検証環境
python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1(Python 3.14.6)、ネットワーク不要- 再現:メモリ上に
../escapeメンバーを含む tar を作り、一時ディレクトリへextractall()してtarfile.OutsideDestinationError: '../escape' would be extracted to ...が出て終了コード 1(末尾の展開先パスは実行ごとに変わるので、シグネチャはwould be extracted toまで) - 修正:メンバー名を
escape(展開先に収まる相対パス)にして、同じ Python 3.14.6 で終了コード 0・シグネチャ消滅。filter='fully_trusted'で検査を無効化する修正は採っていません - 3.13 の既定展開が実際に展開先の外へファイルを書くこと、
fully_trustedが 3.14 でも相対パス・絶対パス・外向き symlink の 3 通りすべてを外へ出すこと、dataが 3 通りすべてを止めること、tarは相対・絶対のトラバーサルは止めるが外向き symlink は作ること、3.12・3.13 でfilter='data'を渡すと 3.14 と同じ例外が出ることは、python:3.12-slim(3.12.13)・python:3.13-slim(3.13.14)・3.14.6 の 3 版でそれぞれ実行して確認しました - 3.12 と 3.13 の間、3.13 と 3.14 の間のパッチ版は測っていません