パストラバーサル(ディレクトリ・トラバーサルとも呼ばれます)は、外から受け取ったファイル名でサーバーの中のファイルを指すときに、決めたフォルダの外のファイルまで指されてしまう問題です。ファイルのアップロードでは、「どんな名前で保存するか」と「保存したファイルをどう取り出すか」の 2 か所で、この問題が関わります。守りの考え方は、大きく 3 つです。保存する名前を外から決めさせないこと、名前でファイルを読み書きするなら「決めたフォルダの内側か」を確かめること、拡張子とサイズを絞ることです。
この記事は、ファイルを受け取る機能や、ファイル名を指定して読み出す機能を作る人向けです。IPA(情報処理推進機構)と OWASP(Web アプリの安全を扱う非営利の団体)の文書、CWE(ソフトウェアの弱点に付く共通の番号)の定義に沿って、守り方を整理します。保存先の内側かの確かめ、保存名の作り直し、拡張子とサイズの絞り方は、Python で動かして確かめました(2026年10月時点)。あわせて、2026年7月に注意喚起が出た Rails の Active Storage の件を取り上げます。この件は、開発元の告知が説明する原因がパストラバーサルとは別で、付いた CWE も CWE-22 ではありません。どこが違うのかも書きます。
書いてあることは、IPA・OWASP・CWE・Python・Node.js・JPCERT/CC などの文書を、2026年10月21日に開いて確かめたものです。手元の確かめは、macOS 14.4.1(arm64)・Python 3.12.3 で、作業用のフォルダの中だけで流しました。Node.js は手元で流していないので、文書に書いてあることまでを紹介します。Windows では確かめていません。
この記事には、攻撃に使える入力は載せていません。上の階層を指す記号の並びや、そのエンコードの形、拡張子のチェックをすり抜ける例、試し方は書きません。記号そのものの名前(「..」が何を指すか)と、photo.png・my report.txt のような無害な名前だけを使います。
- パストラバーサルとは何か。ファイルのアップロードで、名前と保存先のどこが危ないのか
- IPA は「フォルダ名を取り除く」、OWASP と CWE は「正規化してから確かめる」と、守りの置き方が違うこと
- Python の
resolve()とis_relative_to()で、保存先の内側かを確かめた結果。resolve()を省くと誤って判定すること - 保存名をサーバーで作る(UUID)・拡張子の許可リスト・サイズの上限を、手元の例で確かめた結果と、OWASP の保存先の優先順位
- 2026年の Rails の Active Storage の件(CVE-2026-66066)で、開発元の告知が説明する原因がパストラバーサルとは別である点と、直った版、上げたあとにすること
パストラバーサルとは?ファイルのアップロードで何が危ないの?
IPA・OWASP・CWE の説明
IPA は、次のように説明しています。外部からのパラメータ(外から渡される値)で、サーバーの中のファイル名を直接指定している Web アプリでは、ファイル名の指定の作りに問題があると、攻撃者に任意のファイルを指定されて、アプリが意図しない処理をしてしまうことがあります。この問題の一種が「ディレクトリ・トラバーサルの脆弱性」で、それを悪用する攻撃の 1 つが「ディレクトリ・トラバーサル攻撃」です。
起こりうる脅威として、IPA は「サーバ内ファイルの閲覧、改ざん、削除」を挙げています。重要な情報の漏えい、設定ファイル・データファイル・プログラムのソースコードなどの改ざんや削除です。運営している組織やサイトの性質を問わず起こりうる問題で、個人情報などの重要な情報をサーバーにファイルとして保存しているサイトは、特に注意が必要だと書いています。IPA が例に挙げているのは、デザインのテンプレートをファイルから読み込む作りと、利用者の入力内容を指定のファイルへ書き込む作りです。読む場合も書く場合も、当てはまります。
OWASP(community のページ)は、パストラバーサル攻撃を、Web の公開のフォルダ(web root)の外にあるファイルやフォルダに届こうとするものだと説明しています。ファイルを指す変数を書き換えたり、絶対パス(ドライブやルートから始まる、全部を書いたパス)を使ったりして、アプリのソースコード・設定・システムの大事なファイルなど、任意のファイルに届く可能性があります。届くかどうかは、OS のアクセス制御にも左右される、とも書いています。
CWE-22 の定義は、外からの入力で、決まった親のフォルダの下を指すつもりのパス名を組み立てるのに、パス名の中の特別な要素を無害にしていないので、決まったフォルダの外を指してしまう、というものです。特別な要素の例として、「..」とスラッシュの区切りを挙げています。「..」は、ひとつ上のフォルダを指す記号です。
ファイルのアップロードで、どこが関わるの?
OWASP の WSTG(Web アプリのテストの手引き)は、入力を確かめ損ねる入口を数えるとき、HTTP の GET・POST のほか、ファイルのアップロードや HTML のフォームのように、利用者から中身を受け取る所を全部挙げるよう書いています。ファイルのアップロードは、利用者から中身(ファイルとその名前)を受け取る入口です。
利用者が付けたファイル名をそのまま保存先のフォルダにつなぐ作りや、アップロードしたファイルをファイル名で指定して取り出す作りは、IPA の言う「外部からのパラメータにウェブサーバ内のファイル名を直接指定している」作りに当たりえます。
アップロードされたファイルで起きうること
OWASP の File Upload Cheat Sheet(開発者向けの短い手引き)は、悪いファイルで起きうることを次のように挙げています。
- ファイルを読む部品・処理する部品の穴を突く
- フィッシング(本物に見せかけて情報を盗む手口)に使う
- ZIP 爆弾・XML 爆弾(小さなファイルが展開すると巨大になる細工)や、巨大なファイルで保存先を埋めて、サーバーを止める
- 既存のファイルを上書きする
- 公開されていれば、XSS(他の利用者のブラウザーで動くスクリプト)など、他の利用者に効く中身を置く
そのうえで、OWASP は「決め手は 1 つではない」と書いています。守りを重ねる(defense in depth)ことが大事で、1 つの技法では足りない、という考え方です。この記事も、1 つの対策で済ませず、順に重ねる形で説明します。
OWASP Top 10:2025 での位置
OWASP Top 10:2025 では、パストラバーサル(CWE-22)は「A01:2025 アクセス制御の不備」に、危ない種類のファイルの無制限なアップロード(CWE-434)は「A06:2025 安全性を欠いた設計」に対応づけられています。Top 10 では、別の区分に置かれています。
守り方は?IPA は「フォルダ名を取り除く」、OWASP と CWE は「正規化して確かめる」
守り方は、IPA と OWASP・CWE で、置き方が少し違います。揃えずに、両方を並べます。
IPA「安全なウェブサイトの作り方」の 1.3
IPA のページは、対策を「根本的解決」と「保険的対策」に分けています。
| 区分 | 対策 | 中身 |
|---|---|---|
| 根本的解決 | 3-(i)-a | 外部のパラメータでサーバー内のファイル名を直接指定する作りを避ける。本当に必要か、別の方法で代われないかを、仕様や設計から見直す |
| 根本的解決 | 3-(i)-b | ファイルを開くときは固定のフォルダを指定し、かつファイル名にフォルダ名が含まれないようにする |
| 保険的対策 | 3-(ii) | サーバー内のファイルへのアクセス権限を、正しく管理する |
| 保険的対策 | 3-(iii) | ファイル名をチェックする |
3-(i)-b で、IPA は、固定のフォルダ名を前に付けて open(dirname+filename) の形にするだけでは足りない、と書いています。そこで、basename() のように、パス名からファイル名だけを取り出す API(関数)を使い、open(dirname+basename(filename)) の形にして、渡されたパス名からフォルダ名を取り除くよう勧めています。open(filename) の形で書いていると、filename に絶対パス名が渡されたとき、任意のフォルダのファイルが開いてしまうおそれがある、とも書いています。
3-(ii) は、アプリが任意のフォルダのファイルを開こうとしても、サーバー側の機能で断れる場合がある、という守りです。
3-(iii) は、ファイル名を受け取る入力の値から、OS のパス名の読み方でフォルダを指せる文字列を見つけたら、処理をやめる守りです。ただし、URL のデコード(% で表した文字を元に戻す処理)をしている場合は、エンコードした形も、ファイルを指す文字列として有効になることがあります。IPA は、チェックする時機に注意するよう書いています。IPA はこのチェックを、根本的解決ではなく保険的対策に置いています。
OWASP と CWE-22 の守り方
OWASP(community)の「How to protect yourself」は、次のような守りを挙げています。
- ファイルを操作するときは、できるだけ利用者の入力を使わない
- テンプレートや言語ファイルを選ぶときは、ファイル名の一部ではなく、番号(インデックス)を受け取る
- パスの全部を利用者に渡させず、アプリ側のパスで囲む
- 「知っている良い値」だけを受け入れる(データを無害化して通すことはしない)
- どうしても入力を使うなら、ファイル操作の API に渡す前に、
normalize()のような関数で正規化する
また、大事な設定ファイルを Web の公開のフォルダの中に置かないことも挙げています。
CWE-22 の対策(Potential Mitigations)には、次のような項目があります。
- 入力はすべて悪意があるものと見て、「知っている良い値」の許可リストで確かめる
- ファイル名を確かめるときは、使える文字を厳しく絞る。できればピリオドは 1 つだけにして、フォルダの区切りは入れない。拡張子は許可リストで絞る
- 入力は、確かめる前にデコードし、正規化する。言語に組み込みの、パスの正規化の関数(C の
realpath()など)で、「..」とシンボリックリンク(別の場所のファイルを指す、印のようなファイル)を取り除いた、正規のパス名にする - 受け付けるファイル名が決まっているなら、番号の ID から実際のファイル名への対応表を作り、それ以外は断る
- 必要最小限の権限でコードを動かす
さらに CWE-22 は、危ない文字を取り除く仕組みだけに頼らないよう書いています。拒否リスト(危ないものを並べて断る方式)と同じで、抜けが出ることがあるからです。例として、スラッシュを取り除いても、バックスラッシュ(\)をフォルダの区切りに使えるファイルシステムでは、守りとして足りない、と書いています。WAF(Web アプリの手前で通信を見張る仕組み)については、効き目は「Moderate」で、全部の入口を覆うとは限らない、としています。
並べるとどうなる?
IPA の根本的解決は、外から来た名前をそのままファイル名にしない、フォルダ名を取り除く、という守りです。OWASP と CWE は、使う前に正規化して、決めた場所の内側かを確かめる、という守りです。IPA は、記号を見つけて止める確かめを保険的対策に置き、CWE は、危ない文字を取り除く仕組みだけに頼らないよう書いています。この記事は、どちらかに揃えません。次の節で、後者を Python で確かめます。
保存先の内側か、どう確かめる?Python の resolve と is_relative_to で試す
OWASP と CWE が勧める「正規化してから確かめる」を、Python の標準ライブラリ(pathlib)で試します。やることは 3 つです。保存先のフォルダを、先に絶対パスにしておきます。受け取った名前を保存先につないで、resolve() で正規のパスにします。そのパスが保存先の内側かを、is_relative_to() で確かめます。
使う関数の説明(Python の文書)
Path.resolve():パスを絶対パスにして、シンボリックリンクをたどって解決します。パスの中の「..」を取り除くのは、このメソッドだけだと文書に書かれています。strict=False(既定)のときは、存在しないパスも、できるところまで解決して、残りは存在を確かめずに付け足します。PurePath.is_relative_to(other):このパスがotherの下にあるかを返します。文書は「文字の上だけで判定し、ファイルシステムを見ず、「..」の要素も特別に扱わない」と書いています。Python 3.9 で追加されました。- パスをつなぐとき(
/演算子やos.path.join())、途中に絶対パスがあると、それより前は無視されます。受け取った名前が絶対パスだと、つないだ時点で保存先が捨てられる、ということです。
手元で流したコード
inside_check.py です。保存先の外を指す名前は、別のファイル outside_names.txt から読んでいます。そのファイルの中身(上の階層を指す記号を含む名前と、絶対パスの名前)は、この記事には載せません。
from pathlib import Path
UPLOAD_DIR = Path("uploads").resolve() # 保存先のフォルダ(先に絶対パスにしておく)
UPLOAD_DIR.mkdir(exist_ok=True)
def is_inside(name: str) -> bool:
target = (UPLOAD_DIR / name).resolve() # join してから resolve
return target.is_relative_to(UPLOAD_DIR)
def is_inside_without_resolve(name: str) -> bool:
target = UPLOAD_DIR / name # resolve しない(くらべるため)
return target.is_relative_to(UPLOAD_DIR)
names = {"photo.png": "photo.png", "my report.txt": "my report.txt"}
outside = Path("outside_names.txt").read_text().splitlines()
names["保存先の外を指す名前(上の階層を指す記号を含む)"] = outside[0]
names["保存先の外を指す名前(絶対パス)"] = outside[1]
# 保存先の中に、外を指すシンボリックリンクを 1 つ置く(名前は無害)
outside_dir = Path("outside").resolve(); outside_dir.mkdir(exist_ok=True)
link = UPLOAD_DIR / "photo-link.png"
if not link.is_symlink():
link.symlink_to(outside_dir / "photo-link.png")
names["photo-link.png(保存先の外を指すリンク)"] = "photo-link.png"
for label, name in names.items():
print(f"{label}: resolve あり={is_inside(name)} resolve なし={is_inside_without_resolve(name)}")
手元の Python 3.12.3 の出力です。
$ python3 inside_check.py
photo.png: resolve あり=True resolve なし=True
my report.txt: resolve あり=True resolve なし=True
保存先の外を指す名前(上の階層を指す記号を含む): resolve あり=False resolve なし=True
保存先の外を指す名前(絶対パス): resolve あり=False resolve なし=False
photo-link.png(保存先の外を指すリンク): resolve あり=False resolve なし=True
結果の読み方
| 名前 | resolve あり | resolve なし |
|---|---|---|
photo.png・my report.txt | True(内側) | True(内側) |
| 保存先の外を指す名前(上の階層を指す記号を含む) | False(外と判定) | True(内側と誤って判定) |
| 保存先の外を指す名前(絶対パス) | False(外と判定) | False(外と判定) |
photo-link.png(保存先の外を指すシンボリックリンク) | False(外と判定) | True(内側と誤って判定) |
resolve()を省くと、記号を含む名前は内側と誤って判定されました。 文書の「文字の上だけで判定し、ファイルシステムを見ず、「..」の要素も特別に扱わない」という説明のとおりです。判定の前にresolve()で正規のパスにしておく理由が、ここにあります。- 絶対パスの名前は、つないだ時点で保存先が捨てられる(上の説明)ので、
resolve()が無くても外と判定されました。 - 保存先の中にあるシンボリックリンクも、
resolve()がリンクをたどるので、外を指していると判定できました。resolve()が無いと内側と判定されます。
Node.js ではどうなる?(文書にあること)
Node.js は、手元で流していません。公式の文書(v26.12 系の文書)に書かれていることだけを、表にしました。
| 関数 | 文書の説明(要点) |
|---|---|
path.resolve() | パスの並びを右から左へ処理して、絶対パスになるまで前に足していきます。最後まで絶対パスにならなければ、今の作業フォルダを使います。結果は正規化されます。説明に、シンボリックリンクをたどるとは書かれていません |
path.normalize() | 「..」と「.」の要素を解決します。POSIX の決まりには厳密には沿っておらず、「..」を取り除くなどの置き換えで、下の OS の解釈と変わりうる、と書かれています |
fs.realpath() | 「.」「..」とシンボリックリンクを解決して、正規のパス名を求めます(realpath(3) に近い動き) |
シンボリックリンクまで含めて確かめたいときに、path.resolve() だけでよいのか、fs.realpath() を使うのかは、この記事では確かめていません。Node.js で作るときは、手元の環境で、無害な名前を使って動きを確かめてください。
確かめていないこと
- Windows での
resolve()の動き(ドライブ名・バックスラッシュ・短い別名)は、確かめていません。 - 判定してからファイルを使うまでの間に、別の利用者や別のプロセスがファイルを差し替える場合(判定と使う間の競合)は、調べていません。
保存名・拡張子・サイズはどう決める?手元の例で確かめる
ここからは、OWASP の File Upload Cheat Sheet に沿って、アップロードを受け取るときの決め方を見ます。名前と保存先の守り(前の 2 つの節)に加えて、拡張子とサイズを絞ります。
保存名は、アプリが作る
OWASP は、ファイル名を、アプリが作った別のものに変えるよう勧めています。UUID/GUID のようなランダムな文字列をファイル名にすることを、「essential(欠かせない)」と書いています。UUID が何かは、UUID とは?仕様や種類についてすぐ分かる大切な話にまとめています。
利用者が付けた名前が業務で必要なときは、次のような制限を付けるよう書かれています。
- 名前の長さの上限を決める
- 使える文字を、英数字・ハイフン・空白・ピリオドのような範囲に絞る
- 先頭のピリオド(隠しファイル)と、連続するピリオド(ディレクトリ・トラバーサルに使われる)を制限する
OWASP は、Windows の短い別名を使って既存のファイルを上書きされる件の説明の中で、保存名をサーバー側で作れば、攻撃者はどのファイルを狙うかを決められなくなる、と書いています。つまり、保存名を作り直す理由の 1 つは、狙うファイルを外から決めさせないことです。
このブログの UUID の記事には、ファイル名に UUID を使うと「同じファイル名のファイルが上書きされることを防止」できる、という説明があります。これも保存名を UUID にする理由のひとつですが、OWASP の理由(名前そのものの危なさを避ける、狙うファイルを決めさせない)とは別の理由です。
拡張子は、許可リストで絞る
OWASP は、業務に必要な拡張子だけを許可するよう書いています。例は、画像のアップロードなら合意した 1 種類だけ、履歴書のアップロードなら docx と pdf だけ、です。一方、危ない拡張子を並べて断る方式は、それだけでは弱い守りだと書いています。CWE-22 の対策にも、拡張子の許可リストを使う(CWE-434 を避ける)という項目があります。
拡張子の確かめは、ファイル名をデコードしたあとに行い、知られているすり抜けを避けるフィルターで行うよう書かれています。自前の正規表現は、詳しくなければ組まないよう、OWASP が注意しています。すり抜けの具体例は、この記事には載せません。
サイズは、上限を決める
OWASP は、保存先の容量を守るために、アップロードのサイズに適切な上限を置くよう書いています。ファイルを展開したり処理したりするなら、上限は展開したあとの大きさで考えます。ダウンロードにも、要求の上限を置くよう書いています(DoS 対策)。なお、OWASP は上限の数値までは書いていません。
手元の例:UUID の保存名・拡張子の許可リスト・サイズの上限
3 つを合わせた Python の例です(save_upload.py)。
import io
import uuid
from pathlib import Path
UPLOAD_DIR = Path("uploads").resolve()
UPLOAD_DIR.mkdir(exist_ok=True)
ALLOWED = {".png", ".jpg"} # 拡張子の許可リスト
MAX_BYTES = 1 * 1024 * 1024 # 1 MiB まで
CHUNK = 64 * 1024
def save(original_name: str, stream) -> str:
ext = Path(original_name).suffix.lower()
if ext not in ALLOWED:
raise ValueError(f"許可していない拡張子: {ext or '(なし)'}")
stored = UPLOAD_DIR / f"{uuid.uuid4()}{ext}" # 保存名はアプリが作る
total = 0
with open(stored, "xb") as f: # 同じ名前があれば上書きしない
while chunk := stream.read(CHUNK):
total += len(chunk)
if total > MAX_BYTES:
f.close()
stored.unlink() # 書きかけを消す
raise ValueError(f"大きすぎる: {MAX_BYTES} バイトを超えた({total} バイト目まで読んで止めた)")
f.write(chunk)
return stored.name
cases = [
("my report.png", b"\x00" * (512 * 1024)),
("photo.PNG", b"\x00" * 1000),
("notes.txt", b"hello"),
("big.jpg", b"\x00" * (2 * 1024 * 1024)),
]
for name, data in cases:
try:
print(f"{name!r} -> 保存名 {save(name, io.BytesIO(data))}")
except ValueError as e:
print(f"{name!r} -> 断った: {e}")
print("uploads の中身:", sorted(p.name for p in UPLOAD_DIR.iterdir() if not p.is_symlink()))
手元の Python 3.12.3 の出力です。UUID は、流すたびに変わります。
$ python3 save_upload.py
'my report.png' -> 保存名 d76ba80f-10bd-4d72-87e8-6dd030fe63ee.png
'photo.PNG' -> 保存名 42a4c6ea-399b-4a1a-a5dc-dc1123ae202e.png
'notes.txt' -> 断った: 許可していない拡張子: .txt
'big.jpg' -> 断った: 大きすぎる: 1048576 バイトを超えた(1114112 バイト目まで読んで止めた)
uploads の中身: ['42a4c6ea-399b-4a1a-a5dc-dc1123ae202e.png', 'd76ba80f-10bd-4d72-87e8-6dd030fe63ee.png']
出力の読み方です。my report.png は UUID の名前で保存され、元の名前は保存名に入りません。photo.PNG は拡張子を小文字にして保存されました。notes.txt は、許可リストに無い拡張子なので断られました。2 MiB の big.jpg は、上限(1 MiB = 1,048,576 バイト)を超えたので断られ、書きかけのファイルは消されました。64 KiB ずつ読むので、17 回目の読み込み(65,536 × 17 = 1,114,112 バイト)で上限を超えたと分かり、そこで止めています。保存先に残ったのは、通った 2 つだけです。
この例の限界も書いておきます。
- 中身は 0 のバイトの並びで、画像ではありません。拡張子だけで通しています。 ファイルの中身の確かめはしていません(次の節で説明します)。
- サイズの上限は、メモリ上のデータ(
io.BytesIO)で試したものです。Web サーバーやフレームワークの手前で効くリクエストの大きさの上限は、試していません。 - 上限の 1 MiB は、この例のために決めた数字です。OWASP が示した数字ではありません。
保存先と公開のしかたは?中身の確かめは?(OWASP の手引きから)
どこに保存する?
OWASP は、保存先を、安全な順に次のように並べています。
- 別のホストに保存する。ユーザーに応答するアプリと、アップロードを受けて保存するホストを、完全に分けられます
- Web の公開のフォルダ(webroot)の外に保存する。管理者のアクセスだけを許します
- 公開のフォルダの中に保存し、書き込みの権限だけを付ける。読むなら、内部の IP や認可された利用者のような制御が欠かせません
また、アップロード先のフォルダに、Web サーバーの設定ファイル(.htaccess・web.config など)を上げられると、そのフォルダの扱いを変えられるおそれがある、と注意しています。二段目の守りとして、アップロード先ではフォルダごとの設定の上書きを止めるよう勧めています(Apache の AllowOverride None など)。
公開して返すときは、対応表を通す
アップロードしたファイルを、利用者に見せたり、ダウンロードさせたりする場合、OWASP は、アプリの中で ID とファイルを結びつける対応表を通すよう書いています(someid -> file.ext の形)。利用者から来た値は ID で、実際のファイル名への結びつきはアプリの中で持つ形です。CWE-22 の「番号の ID から実際のファイル名への対応表を作り、それ以外は断る」と同じ方向の守りです。
アップロードを受けるときは、アップロードの前に、認証(誰か)と認可(そのファイルを触ってよいか)の 2 段で確かめるよう書かれています。認証と認可の違いは、認証と認可の違いとは?で説明しています。ファイルの権限は、最小にします。アップロードを CSRF(ログイン中の利用者に、意図しない操作をさせる攻撃)から守ることも、一覧に挙がっています。
上げたファイルを公開で返すときの応答のヘッダー(X-Content-Type-Options: nosniff など)は、同じ回のセキュリティヘッダーの記事で扱います。
中身はどう確かめる?
| 確かめ方 | OWASP の書き方 |
|---|---|
| Content-Type を見る | 利用者が送ってくる値で、偽るのは簡単なので、信じられません。安全の拠り所にはしませんが、うっかり違う種類を上げるのを止める早い確かめにはなります |
| ファイルの署名(先頭のバイト)を見る | それだけで使いません。すり抜けはよくあり、簡単です |
| 画像を作り直す | 画像をデコードして、許した形式に作り直し、要らないメタデータを取り除きます。ただし、作り直しで悪い中身が全部消える保証はなく、画像を処理する部品そのものが、信用できない入力を扱います。新しいライブラリを、形式を絞り、資源の上限を付け、サンドボックスで使うよう書かれています |
| ZIP | 勧めません。あらゆる種類のファイルを含められ、攻撃の経路が多いからです |
ほかに、OWASP の一覧には、ウイルス対策やサンドボックスに通すこと、PDF・DOCX のような種類には CDR(ファイルを分解して、安全な部分だけで組み直す処理)に通すこと、使っているライブラリを安全に設定して新しく保つことも挙がっています。
この節の確かめ(Content-Type・署名・画像の作り直し・ウイルス対策)は、手元では流していません。OWASP の書き方を紹介しただけです。手元の例(前の節)は、拡張子の許可リストとサイズの上限だけです。次の節の Rails の件は、この「画像を処理する部品そのものが信用できない入力を扱う」という一文と、同じ方向の話です。ただし、OWASP がこの件を挙げているわけではありません。
2026年に注意喚起があった?Rails の Active Storage の件と、パストラバーサルとの違い(2026年10月時点)
何が公表されたの?
JPCERT/CC は、注意喚起 JPCERT-AT-2026-0021 を、2026年7月30日に公開し、8月3日に更新しています。2026年7月29日(現地時間)に、Ruby on Rails の Active Storage(ファイルのアップロードを扱う Rails の機能)で、リモートコード実行(外から、サーバーでプログラムを動かされること)につながる脆弱性(CVE-2026-66066)が公表された、という内容です。悪用されると、遠隔の第三者が細工したファイルをアップロードして、サーバー上のファイルや認証情報を読み取り、リモートコード実行に至る可能性があります。
Rails の開発元の告知(rails/rails の GitHub Security Advisory、2026年7月29日公開、深刻度は critical)は、こう説明しています。既定の設定で、画像の variant(元の画像から作る、大きさや形式を変えた派生画像)を表示する Rails アプリは、認証なしの攻撃者に、サーバーの任意のファイル(プロセスの環境変数を含む)を読まれるおそれがあります。環境変数には、secret_key_base(Rails が署名や暗号化に使う秘密の値)や外部システムの資格情報が入っていることが多く、リモートコード実行や、そのシステムへの横の移動につながるおそれがあります。
開発元の告知が説明する原因は、パストラバーサルとは別
任意のファイルを読まれるという結果は、パストラバーサルに似ています。しかし、開発元の告知が説明する原因は別です。 告知によると、画像を処理する libvips(画像処理のライブラリ)は、ファイル形式を「ローダー」と「セーバー」で読み書きし、その多くはサードパーティのライブラリが支えています。libvips は、一部に「unfuzzed」の印を付けています。信用できない中身には安全でない、という意味です。Active Storage は、その unfuzzed の処理を無効にしていませんでした。 そのため、細工したファイルをアップロードして variant を作らせると、それを呼べるおそれがありました。
ファイル名やパスの確かめ不足ではなく、Active Storage が、libvips の危ない処理を止めていなかった(既定の設定の)問題です。告知に付いた CWE も、CWE-22(パストラバーサル)ではなく、CWE-1188(安全でない既定値で資源を初期化する)です。前の節の OWASP の一文「画像を処理する部品そのものが、信用できない入力を扱う」と、同じ方向の話です。
当たる条件と、直った版
開発元の告知は、次の 2 つの条件を満たすアプリが当たる、と書いています。
- Active Storage の画像処理に libvips を使っている(
config.active_storage.variant_processor = :vips。load_defaults 7.0で設定され、以後の既定は変わっていません) - 信用できない利用者から、画像のアップロードを受けている
variant を作ることは、別の条件ではありません。JPCERT/CC の注意喚起は、これに加えて、特定のサードパーティのライブラリを使う libvips で、unfuzzed と分類されたファイルローダーが有効であることを、3 つ目の条件に挙げています。条件の数え方が資料で違うので、この記事は開発元の告知の 2 つを柱にしました。JPCERT/CC は、direct upload(ブラウザーから直接アップロードする機能)が既定で有効で、アプリ固有の認証・認可が標準では付いていないため、利用者向けのアップロード機能が無いアプリでも当たる可能性がある、とも書いています。
直った版は、次のとおりです(開発元の告知・JPCERT/CC・NVD で一致しています)。
| 系統 | 影響を受ける版 | 直った版 |
|---|---|---|
| 7.2 系 | activestorage 7.2.3.2 より前 | 7.2.3.2 |
| 8.0 系 | 8.0 から 8.0.5.1 より前 | 8.0.5.1 |
| 8.1 系 | 8.1 から 8.1.3.1 より前 | 8.1.3.1 |
上げるときと、上げたあとにすること
- libvips は 8.13 以上、ruby-vips は 2.2.1 以上に、先に上げます。 JPCERT/CC は、libvips が 8.13 未満および ruby-vips が 2.2.1 未満の場合、Active Storage の起動時に例外が出てアプリが起動しないので、先に libvips を 8.13 以上・ruby-vips を 2.2.1 以上に上げるよう書いています。開発元の告知は、libvips が 8.13 未満だと unfuzzed の処理を止められないので、起動時に例外を出す、と書いています。
- 直った版の activestorage に上げます。
- 秘密を変えます。 上げることで穴はふさがりますが、すでに持ち出された秘密は戻りません。開発元の告知は、アプリのプロセスが読める秘密は、漏れた可能性があるものとして変えるよう書いています。例は、
secret_key_base・マスターキーとconfig/credentials.yml.encの中身・Active Storage のサービス(S3・GCS・Azure)の鍵・DB の資格情報・外部サービスのトークンと鍵です。JPCERT/CC も、当たりうる環境は侵害を受けた前提で、読み取れる認証情報をすべて変えるよう推奨しています secret_key_baseを変えると、いまのセッションが切れて、利用者は再ログインになります。暗号化・署名つきの Cookie、signed global ID、Active Storage の URL にも影響します
深刻度と、悪用されたか
- 深刻度: Rails の告知(GitHub の Security Advisory)では、CVSS 4.0 で 9.5 です。NVD(アメリカの脆弱性データベース)では、この点数の出どころが GitHub(security-advisories@github.com)と表示されています。CVSS は脆弱性の深刻さを点数にする仕組みです。NVD は、2026年10月21日に取ったデータで、状態が「Awaiting Analysis(分析待ち)」で、NVD 自身の採点はありません。JPCERT/CC の注意喚起に、点数はありません
- 悪用: JPCERT/CC は、エクスプロイトコード(脆弱性を突くプログラム)が公開されているのを確認していて、今後、悪用する攻撃が広く行われる可能性があると書いています。一方、CISA の KEV(悪用が確認された脆弱性の一覧、catalogVersion 2026.10.20)に、この CVE は載っていません。この記事は、この件が悪用されたとは書きません。 資料にあるのは「公開されている」「可能性がある」までです
ほかの 2026 年の件(社名・製品名は出しません)
アップロードやパストラバーサルに関わる、2026年の公的機関の文書も探しました。数えたのはこの記事で、機関の集計ではありません。
- JPCERT/CC の 2026 年の注意喚起 32 件(at260001〜at260032)のうち、本文に「アップロード」の語があるのは at260021(Rails)だけ、「トラバーサル」の語があるのは at260014 だけでした
- JPCERT-AT-2026-0014(2026年5月21日):開発元が、エンドポイントのセキュリティ製品(オンプレミス版)の相対パストラバーサルの脆弱性(CVE-2026-34926)を悪用した攻撃を確認している、と書かれています。KEV にも同じ日に載っています。どの機能で何が起きたのかは文に書かれておらず、この記事では確かめていません。アップロードの件でもありません
- IPA の重要なセキュリティ情報(2026年2月25日):エンドポイント管理の製品(オンプレミス版)にパストラバーサルの脆弱性があり、製品を入れた Windows の任意のファイルを改ざんされ、任意のコードを実行されるおそれがあります(CVE-2026-25785、CVSS v3 で 9.8、IPA の区分は「緊急」)。IPA は、悪用を確認したとは書いていません。この CVE は KEV に載っていません。IPA の 2026 年度の一覧にある 24 件の本文には、「アップロード」の語がありませんでした
- KEV に 2026年1月7日から 10月20日までに足された 270 件のうち、
cwesの欄に CWE-434 があるのは 7 件、CWE-22・CWE-23・CWE-36 のどれかがあるのは 17 件です(両方ある 2 件を重ねずに数えると 22 件。cwesが空の 9 件は数に入っていません)。KEV のdateAddedは、悪用が確認されて一覧に足した日で、脆弱性が出た年ではありません
CWE-434 の 7 件の CISA の説明は、認証なしで任意のファイルを上げて動かされるもの、メールサーバーの任意の場所にファイルを上げられるもの、任意のファイルを書く・相対の場所にデータを上げるもの(2 件。CWE-22 と CWE-434 の両方)などです。原因のコードまでは書かれていません。
よくある質問
Rails の Active Storage の件は、悪用された攻撃が確認されていますか?
この記事で開いた資料からは、分かりません。JPCERT/CC は、エクスプロイトコードが公開されているのを確認していて、今後、悪用する攻撃が広く行われる可能性があると書いています。CISA の KEV(catalogVersion 2026.10.20)には、この CVE は載っていません。「悪用された」とは書かれていない、という意味です。
Node.js や Windows でも、同じ書き方で守れますか?
Node.js は、文書の説明を紹介しただけで、手元では流していません。path.resolve() の説明に、シンボリックリンクをたどるとは書かれていない点は、確かめが要ります。Windows は、ドライブ名・バックスラッシュ・短い別名を含めて、確かめていません。どちらも、無害な名前を使って、自分の環境で確かめてください。
署名つき URL で S3 に直接アップロードする形でも、種類やサイズの確かめは要りますか?
この記事では、確かめていません。このブログの署名つき URL の記事は、署名つき URL を、S3 への一時的なアクセス許可を持つ URL で、アップロードなども許可できるものとして説明していますが、上げたファイルの種類・サイズ・名前の確かめは扱っていません。AWS の文書も、この記事では開いていません。Rails の direct upload とは、別の仕組みの話です。
- 外から来たファイル名を、そのまま保存名やパスに使っていないか。保存名を、アプリが作っているか(→「保存名は、アプリが作る」)
- 外から名前を直接指定させない作りにできないか。ファイルを返すなら、番号の ID と実際のファイルの対応表を、アプリの中に持てないか(→「守り方は?IPA は「フォルダ名を取り除く」、OWASP と CWE は「正規化して確かめる」」)
- 名前でファイルを読み書きする所で、使っている言語の、パスを正規化する関数を通してから、決めたフォルダの内側かを確かめているか(→「保存先の内側か、どう確かめる?Python の resolve と is_relative_to で試す」)
- 拡張子を、許可リストで絞っているか。危ない拡張子を並べて断る方式だけにしていないか(→「拡張子は、許可リストで絞る」)
- サイズの上限があるか。展開や処理をするなら、展開したあとの大きさで考えているか(→「サイズは、上限を決める」)
- 保存先は、別のホストか Web の公開のフォルダの外か。アップロード先で、フォルダごとの設定の上書きを止めているか(→「どこに保存する?」)
- 公開して返すとき、ID とファイルの対応表を通しているか。アップロードの前に、認証と認可を確かめているか(→「公開して返すときは、対応表を通す」)
- 画像などを処理する部品(Active Storage・libvips など)の版を把握しているか。告知が出たら、直った版に上げて、秘密を変える段取りがあるか(→「上げるときと、上げたあとにすること」)
まとめ
- パストラバーサルは、外から受け取ったファイル名で、決めたフォルダの外のファイルまで指されてしまう問題です。ファイルのアップロードでは、保存する名前と、名前で取り出す機能が関わります
- IPA は、外から名前を直接指定する作りを避けることと、ファイル名からフォルダ名を取り除くことを根本的解決に置きます。OWASP と CWE は、正規化してから、決めた場所の内側かを確かめるよう勧めます。この記事は、両方を示しました
- Python では、
resolve()で正規のパスにしてからis_relative_to()で判定しました。resolve()を省くと、記号を含む名前も、外を指すシンボリックリンクも、内側と誤って判定されました - 保存名は UUID のようにアプリが作り、拡張子は許可リストで絞り、サイズには上限を置きます。保存先は、別のホストか Web の公開のフォルダの外にします。守りは 1 つで済ませず、重ねます
- Rails の Active Storage の件(CVE-2026-66066)は、開発元の告知では、Active Storage が libvips の危ない処理を止めていなかった(既定の設定の)問題とされ、付いた CWE は CWE-22 ではなく CWE-1188 です。直った版は 7.2.3.2・8.0.5.1・8.1.3.1 で、libvips は 8.13 以上に上げ、上げたあとも読める秘密を変えます
- この件は、エクスプロイトコードが公開されていますが、KEV には載っておらず、悪用されたとは書かれていません。CVSS 4.0 の 9.5 は Rails の告知に付いた点数で、NVD 自身の採点ではありません
- Node.js は文書の範囲までで、手元では流していません。Windows は確かめていません
全体の点検は、Web 開発のセキュリティの基本の記事にまとめています。上げたファイルを公開で返すときのヘッダーは、同じ回のセキュリティヘッダーの記事をご覧ください。
参考資料
- IPA:安全なウェブサイトの作り方 - 1.3 パス名パラメータの未チェック/ディレクトリ・トラバーサル
- IPA:安全なウェブサイトの作り方(全体のページ)
- OWASP:File Upload Cheat Sheet
- OWASP:Path Traversal
- OWASP WSTG:Directory Traversal File Include
- OWASP Top 10:2025 A01 Broken Access Control
- OWASP Top 10:2025 A06 Insecure Design
- CWE-22: Path Traversal
- CWE-434: Unrestricted Upload of File with Dangerous Type
- Python:pathlib
- Node.js:path
- Node.js:fs
- JPCERT/CC:Ruby on Rails の Active Storage における脆弱性(JPCERT-AT-2026-0021)
- Rails の告知(GitHub Security Advisory GHSA-xr9x-r78c-5hrm)
- NVD:CVE-2026-66066
- CISA:Known Exploited Vulnerabilities カタログ(JSON)
- JPCERT/CC:2026年の注意喚起の一覧
- JPCERT/CC:JPCERT-AT-2026-0014
- IPA:重要なセキュリティ情報(2026年2月25日)
- IPA:重要なセキュリティ情報(2026年度の一覧)
※この記事は、公式の資料をもとに AI(Claude)で下書きし、2026年10月21日時点の公式の資料と照らして確かめてから公開しています。誤りに気づいたらお問い合わせからお知らせください。



