Error [tarfile.OutsideDestinationError: '../escape' would be extracted to]

tarfile.OutsideDestinationError: would be extracted to ... which is outside the destination の直し方(Python 3.14)

FIX SUMMARY verified
Applies when
python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1Extracting a tar member that escapes the destination (../ path or outward symlink); Python 3.14 defaults the extraction filter to 'data', which rejects it instead of writing outside the destination

Verified: reproduced in python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1, then the tarfile.OutsideDestinationError: '../escape' would be extracted to signature was gone after the fix (exit 0).

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 destination
  • tarfile.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 が入っている

../escapeoutput の外へ実際に書き出されます。 展開先の親ディレクトリにファイルができます(実測: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 の既定)tarfully_trusted
../escape(相対)拒否拒否外へ書き出す
/tmp/abs-escape(絶対)展開先内に収める展開先内に収める外へ書き出す
外を指す symlink拒否作成する作成する

読み方は 2 つです。

  • data(既定)は 3 通りすべてを止めます。 3.14 の既定が data になったことが、この境界の実体です。
  • fully_trusted は 3 通りすべてを素通しします。 相対パスも絶対パスも、そして symlink も、展開先の外へ出ます。

tar は中間で、相対パスと絶対パスのトラバーサルは止めますが、外を指す symlink は止めません(作成します)。datatar の差はここです。

絶対パスも危険です。 datatar が先頭の / を落として展開先内に収めるので安全に見えますが、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 の間のパッチ版は測っていません

検証(machine-verified)

この修正は python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1 のバージョン固定コンテナ内で再現し、修正後に tarfile.OutsideDestinationError: '../escape' would be extracted to のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs python/tarfile-outside-destination-python-314
● reproduce tarfile.OutsideDestinationError: '../escape' would be extracted to present ✓
● apply fix exit 0
● re-run tarfile.OutsideDestinationError: '../escape' would be extracted to gone ✓
PASS verified · python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1 · signature gone

確認したのは上のイメージの中だけです。別の環境で直らなかった、記述が違う、という場合は 報告してください(対象と検証イメージは件名・本文に入ります)。