ゆめさくセキュリティ

レッスン 2-6

ファイルの受け取りとパス

拡張子を確かめても、実行される場所では意味を持たないことがあります。

受け取ったファイルの名前を、そのまま保存先の住所につなぐ穴 (パストラバーサル) を扱います。名前ではなくアプリが振った ID で保存し、正規化のあとで置き場の中かを確かめる作りと、確かめた時と使う時のずれ、置き場の外に届かないことを固める試験までを扱います。

  • 目安60 分
  • 出典を確かめた日2026年10月24日
  • 修了の条件確かめた時と使う時のずれが問題になる例を挙げられる

なぜ通るのか

見積書を送ってもらう画面を作ったとします。サーバーは、受け取ったファイルを置き場のフォルダに保存し、あとで同じ名前で取り出せるようにします。いちばん短く書くと、置き場のフォルダの住所に、利用者が付けたファイルの名前をつないで、そこに書き込む形になります。

ここで、ファイルの名前は利用者が自由に決められる値です。そしてファイルの住所 (パス) の中では、名前の一部がただの文字ではなく、「どのフォルダか」を指す命令として読まれます。値のはずの名前が、置き場所を決める命令に混ざります。

名前が場所の命令になる所

パスの区切りの文字 (/) を含む名前は、フォルダを 1 段下りる指示として読まれます。「1 つ上のフォルダ」を表す書き方 (..) を含めば、置き場から外へ出る指示になります。IPA の「安全なウェブサイトの作り方」は、固定のフォルダの名前とファイルの名前をつなぐだけでは、この上の階層を指す書き方を使った攻撃を防げないと書いています。

悪意が無くても起きます。「2026/見積.pdf」という名前で送られると、置き場の中に「2026」というフォルダが無いので保存に失敗します。失敗するということは、名前が保存の場所を動かせるということです。

さらに Python の os.path.join は、2 つ目に絶対パス (根から始まる住所) が来ると、1 つ目の置き場を捨てます。名前しだいで、置き場とは関係のない場所に書き込めます。

確かめた時と使う時のずれ

「名前に区切りが入っていないか確かめてから保存する」という守りは、確かめる時の名前と、使う時の名前が同じである間しか効きません。ずれが起きる形は、たとえば次の 3 つです。

  • 確かめたあとに、名前の符号 (% で始まる書き方など) を戻したり、文字の正規化をしたりする。確かめたのは戻す前の文字列で、開くのは戻したあとの文字列になる
  • 確かめた時は普通のファイルだった場所が、開く時までに別の場所へのリンク (シンボリックリンク) に替わる
  • 保存の時に拡張子を確かめても、配信する時にはサーバーの設定が別の規則で扱いを決める。確かめた所と、実行や表示が決まる所が違う

確かめは、使う直前の、正規化の終わった形に対して行います。もっとよいのは、利用者の名前を住所に使わないことです。

脆いコードと直したコード

ラボの中の小さな関数で比べます。Python に付いている物だけで動き、作業用のフォルダの外には書きません。

名前で保存するか、ID で保存するか

脆いコード受け取った名前で保存する
import os def save(base, name, data):path = os.path.join(base, name)with open(path, "wb") as f:f.write(data)return name
直したコードID を振り、名前は別に持つ
import uuidfrom pathlib import Path NAMES = {} def save(base, name, data):fid = uuid.uuid4().hexPath(base, fid).write_bytes(data)NAMES[fid] = namereturn fid

直した方では、保存の住所はアプリが振った 32 けたの英数字だけで決まります。利用者の付けた名前は、一覧に表示するための値として別に持つだけで、住所には一度も入りません。OWASP の File Upload Cheat Sheet も、名前をアプリが作った物に替えること、置き場をウェブの公開フォルダの外か別のサーバーにすることを勧めています。

つないで開くか、正規化してから確かめるか

脆いコード名前をつないで返す
import os def load(base, name):path = os.path.join(base, name)with open(path, "rb") as f:return f.read()
直したコード正規化のあとで置き場の中か確かめる
from pathlib import Path def load(base, fid):root = Path(base).resolve()path = (root / fid).resolve()if not path.is_relative_to(root):raise PermissionError(fid)return path.read_bytes()

resolve は、上の階層を指す書き方もリンクも解いて、最終的に指す住所に直します。直したあとの住所が置き場の中にあるかを確かめるので、確かめる形と開く形がそろいます。それでも、確かめてから開くまでの間にリンクを差し替えられる余地は残ります。だから置き場には、アプリだけが書けるようにし、利用者の名前を住所に使わない、という作りと組み合わせます。

ログでの見え方と検知

脆いコードに「2026/見積.pdf」を渡すと、置き場の中に無いフォルダを開こうとして失敗します。記録には次のような行が残ります。

記録脆いコードのときの記録
FileNotFoundError: [Errno 2]No such file or directory:'uploads/2026/見積.pdf'

この行は、名前が保存の場所を動かしたことの証拠です。守る側は、次の物を記録と見張りに入れます。

見る物なぜ効くか
開こうとした住所の全体置き場の外を指していれば、それだけで異常と分かる
置き場の外を指して拒んだ数直したあとに増えたら、試されている
区切りや符号を含む名前の数ふだんの利用では、ほとんど出ない

記録には正規化のあとの住所を残します。正規化の前の文字列だけだと、実際にどこを開こうとしたのかが分かりません。画面には「保存できませんでした」とだけ返し、住所やフォルダの構成は返しません。

直し方と、直ったことの確かめ方

自分の PC の中だけで行います。外のサービスや、他人の機械には向けません。

  1. 作業用のフォルダの中に、置き場のフォルダ uploads を作る
  2. 脆い save に、作業用のフォルダの中の uploads の外を指す絶対パスを名前として渡し、uploads の外にファイルができることを確かめる
  3. 保存と取り出しを、直したコードに置き換える
  4. 同じ名前を渡し、uploads の外には何もできず、uploads の中に ID の名前のファイルが 1 つできることを確かめる
  5. uploads の中に外を指すリンクを 1 つ置き、直した load が拒むことを確かめる
  6. 下の試験を足し、直す前のコードに戻すと試験が落ちることも確かめる。終わったら作業用のフォルダごと消す
Python置き場の外に届かないことを固める試験
def test_name_cannot_pick_place(tmp_path):base = tmp_path / "uploads"base.mkdir()outside = tmp_path / "outside.txt"fid = save(base, str(outside), b"x")assert not outside.exists()assert (base / fid).read_bytes() == b"x" def test_link_cannot_leave(tmp_path):base = tmp_path / "uploads"base.mkdir()(tmp_path / "secret.txt").write_text("s")link = base / "link"link.symlink_to(tmp_path / "secret.txt")with pytest.raises(PermissionError):load(base, "link")

試験は「同じ入力で、直す前は置き場の外に届き、直したあとは届かない」ことを固定します。リンクの試験は、文字列だけを見る確かめでは止まらず、正規化のあとの住所を見る確かめなら止まる形を固めます。

あわせて、アプリを動かす利用者の権限を、置き場のフォルダにだけ書ける形に絞ります。IPA はこれを保険的な対策に挙げています。穴が残っていても、書ける場所が少なければ、被害は小さくなります。

関わる事例

この穴に合う事例は、事例の一覧にはまだありません。代わりに、この穴の種類について公的機関が出した文書を紹介します。

CISA と FBI は 2024年5月2日に、ソフトウェアからディレクトリトラバーサル (このレッスンの穴の別名) を無くすよう作り手に求める Secure by Design Alert を出しました。文書は、この穴を悪用した最近の広く知られた攻撃 (CVE-2024-1708、CVE-2024-20345) を受けて作ったと述べ、医療を含む重要インフラの分野に影響したと書いています。

その時点で、CISA の悪用が確認された脆弱性の一覧 (KEV カタログ) には、ディレクトリトラバーサルが 55 件載っていました。文書は、避け方は知られているのに悪用が続き、病院や学校の運営にも影響したと指摘しています。

確認クイズ

1 問目

保存の前に、名前に上の階層を指す書き方が入っていないかを文字列で確かめています。そのあとで、名前の % で始まる符号を元の文字に戻してから保存しています。何が問題ですか。

  • ア 確かめる文字列と、保存に使う文字列が違う
  • イ 符号を戻すと、ファイルの中身が壊れる
  • ウ 文字列の確かめは遅いので、要求が詰まる
答えと理由を見る

答え ア

確かめたのは戻す前の文字列で、保存に使うのは戻したあとの文字列です。戻したあとに初めて現れる区切りは、確かめをすり抜けます。確かめは、使う直前の、正規化の終わった形に対して行います。

2 問目

利用者が送ったファイルを、あとで本人がダウンロードできるようにしたい。どう保存するのがよいですか。

  • ア 利用者の付けた名前のまま、公開フォルダに置く
  • イ 利用者の付けた名前から区切りの文字を取り除いて、公開フォルダに置く
  • ウ アプリが振った ID で公開フォルダの外に置き、名前は表示用に別に持つ
答えと理由を見る

答え ウ

ID で保存すれば、利用者の名前は住所に入りません。公開フォルダの外に置けば、配信の設定しだいで実行されたり、そのまま開かれたりすることも避けられます。区切りを取り除くだけでは、取り除き方と正規化の順番しだいで、すり抜ける余地が残ります。

説明課題

次のことを、自分の言葉で 3 行にまとめてください。書けたら、このレッスンは修了です。

  • 確かめた時と使う時のずれが問題になる例を 1 つ挙げ、なぜ問題になるのか
模範解答を見る

名前に区切りが無いことを確かめてから、名前の符号を元の文字に戻して保存すると、確かめた文字列と使う文字列が違ってしまう。戻したあとに現れた区切りは確かめを通り抜け、置き場の外を指す住所として使われる。だから確かめは正規化のあとの住所に対して行い、そもそも利用者の名前を住所に使わない。

一次資料

どれも 2026年10月24日に開いて、題と中身を確かめました。

次へ

次のレッスンは「危ないデシリアライズ」です (準備中)。信じられない入力をオブジェクトに戻すと、データのはずの物がコードとして動く穴を扱います。

次のレッスン 2-7 危ないデシリアライズ 準備中

このレッスンにつながる物