CyberFix Note
セキュアコーディング

パストラバーサルとは何か。基準ディレクトリの外へ解決されるパスの脆弱性と根本対策を解説

対象の目安: Webアプリ開発者 / 実務

アオイ防御・運用担当
・ 約10分で読めます
パストラバーサルとは何か。基準ディレクトリの外へ解決されるパスの脆弱性と根本対策を解説

ファイル名を受け取って中身を返す、テンプレート名を受け取って読み込む、といった「ユーザーの入力からファイルを開く」処理は、Webアプリのあちこちに登場します。この入力を疑わずにファイルパスへ組み込むと、../のようなパス要素によって、本来アクセスさせるべきでないファイルまで読み書きされてしまいます。これがパストラバーサル(ディレクトリトラバーサル、CWE-22)です。

この記事は、ファイルを扱うコードを書くWebアプリ開発者に向けて、パストラバーサルがなぜ成立するのかという機構から、想定される影響、そして正規化(canonicalization)と許可リストを軸とした根本対策までを順に整理します。入力の検証と出力のエスケープという一般原則とは別に、「パスの解決」という固有の落とし穴を扱います。

パストラバーサルが成立する機構

パストラバーサルは、アプリが組み立てるファイルパスの一部にユーザー入力が入り込み、その入力が親ディレクトリを指す要素を含むために、意図した基準ディレクトリの外側のファイルへパスが解決されることで成立します。

具体的に見ます。あるアプリが、ユーザーから受け取ったファイル名を固定のディレクトリに連結してファイルを開くとします。

// 脆弱な例: 受け取った名前をそのまま連結している
const base = "/var/www/uploads/";
const filePath = base + req.query.name; // name にユーザー入力
fs.readFile(filePath, callback);

nameに通常のファイル名が来る限りは問題なく動きます。しかしname../を含む値が入ると、OSはこれを親ディレクトリへの参照として解釈し、連結後のパスが/var/www/uploads/の外を指してしまいます。MITREのCWE-22は、攻撃者が..や区切り文字といった特殊な要素を使うことで、制限された場所の外側にあるファイルへ到達しうると説明しています。PortSwiggerの解説も、ユーザー入力を検証せずにファイルシステムのAPIへ渡すと、サーバー上の任意ファイルを読み取られうるとしています。

原因は、入力が「ファイルの中身を指す単なる名前」ではなく、「パスの構造を書き換えられる文字列」として扱われてしまう点にあります。パスは区切り文字と親参照によって階層を移動できる言語であり、その言語をユーザーに自由に書かせているのと同じ状態になっているわけです。

攻撃で起きること

パスが基準ディレクトリの外へ抜けると、アプリの権限で読めるファイルが読み取りの対象になります。OWASPは、この攻撃がWebルートの外に保存されたファイルやディレクトリへのアクセスを狙うものだと定義しています。

読取だけでも影響は小さくありません。アプリケーションの設定ファイル、データベースの接続情報、ソースコード、OSの設定ファイルなどが該当します。認証情報が書かれたファイルに到達されれば、そこを起点に別の攻撃へつながることもあります。

処理の内容によっては書込側にも及びます。ユーザー入力から決めたパスへファイルを保存したりログを書き出したりする実装では、基準ディレクトリの外へファイルを書き込まれ、既存ファイルの上書きや想定外の場所への配置を許すことがあります。読取と書込のどちらであっても、根の問題は同じで、パスの決定をユーザー入力に委ねてしまっている点にあります。

OWASP Top 10 2025では、パストラバーサル(CWE-22)はアクセス制御の不備を束ねるA01:2025 Broken Access Controlのカテゴリに位置づけられています(2021年版でも同じA01のカテゴリでした)。本来アクセスできる範囲を超えてファイルへ到達される、というアクセス制御の問題として捉えられているためです。

../を除去するだけでは不十分な理由

「入力から../を削除すればよい」という発想は、拒否リスト(denylist)による対処です。この方針は回避されやすく、主たる対策にはできません。

PortSwiggerは、../を単純に取り除く処理が複数の手段で迂回されうると指摘しています。たとえば....//のように入れ子にした並びは、中央の../を一度だけ除去する処理を通すと、残りが../に戻ります。パーセントエンコードや二重エンコードで../を表現して、デコードの順序の隙をつく手段もあります。加えて、区切り文字から始まる絶対パスをそのまま渡す形も、除去では防げません。

問題の本質は、パスの見え方が一つに定まらない点にあります。同じ場所を指すパスに複数の表記が存在し、除去処理が想定する一つの形だけを消しても、別の表記が抜けてしまいます。だからこそ、危険な形を列挙して消すのではなく、パスを一意な形に直してから判断する必要があります。CWE-22も、既知の良い入力だけを受け入れる許可リスト方式と、realpath()のような正規化関数の利用を対策として挙げています。

根本対策の組み立て方

対策は、入力の受け取り方と、パスの検証の二段で組み立てます。ユーザーに直接パスを書かせない設計を土台に、やむを得ずパスの一部を入力から決める場合は正規化して基準ディレクトリ配下に閉じ込めます。

  1. 1

    入力をパスではなく識別子に限定する

    ファイル名を丸ごと受け取るのをやめ、IDや列挙値だけを受け取り、実際のパスはサーバー側の対応表から決めます。ユーザー入力がパスの構造に触れなければ、トラバーサルは起こりません。

  2. 2

    識別子を許可リストで検証する

    受け取る値を、あらかじめ定めた候補や文字種(英数字などに限る)だけに制限します。CWE-22は、この「既知の良い入力を受け入れる」許可リスト方式を推奨しています。

  3. 3

    パスを組む場合は正規化して基準ディレクトリ配下か検証する

    入力を基準ディレクトリに連結したうえで、プラットフォームの正規化APIでパスを一意な形に直し、その結果が基準ディレクトリの配下から始まっているかを確認します。配下でなければ拒否します。

  4. 4

    プロセスの権限を最小化する

    アプリを動かすプロセスに、業務で必要なファイル以外への読み書き権限を与えないようにします。万一パスが抜けても、到達できる範囲を狭められます。

三段目の検証が、除去との決定的な違いです。危険な形を消すのではなく、最終的に解決されるパスを正規化してから、それが許した範囲に収まっているかを確かめます。PortSwiggerも、入力を基準ディレクトリに連結してからファイルシステムAPIで正規化し、正規化後のパスが想定した基準ディレクトリから始まることを検証する、という手順を推奨しています。

注意

正規化してから検証する順序が重要です。検証してから正規化すると、検証時点では安全に見えたパスが正規化で別の場所に解決される余地が残ります。必ず、正規化した後のパスを基準ディレクトリと突き合わせてください。

言語やフレームワークでの実装の勘所

多くの言語には、パスを正規化して一意な形に直すAPIが用意されています。考え方は共通で、シンボリックリンクや..を解決した「絶対パスの正準形(canonical path)」を得て、それが基準ディレクトリの配下かを判定します。

Node.jsであればpath.resolveで絶対パスに直し、基準ディレクトリからの相対をpath.relativeで求めて、結果が基準の外を指さないかを確認する形が基本です。ただしpath.resolveは文字列としての正規化にとどまり、シンボリックリンクは解決しません。基準ディレクトリの内側に置かれたリンクが外部を指す場合に備えるには、fs.realpathで実体のパスまで解決してから範囲を判定します。Pythonではos.path.realpathで実体のパスを得て、os.path.commonpathで基準ディレクトリの配下かを判定できます。C系ではCWE-22が挙げるrealpath()が同じ役割を担います。共通するのは、文字列としてのパスだけを信じず、ファイルシステムが実際に解決する実体のパスまで確定させてから範囲を判定する点です。

// 安全な例: 実体のパスまで解決してから基準ディレクトリ配下か検証する
const path = require("path");
const fs = require("fs");

const base = fs.realpathSync("/var/www/uploads");
// 実在しないパスは例外になるため、呼び出し側で捕捉して拒否側へ倒す
const target = fs.realpathSync(path.resolve(base, req.query.name));
const rel = path.relative(base, target);

// rel が ".." そのもの、".." + 区切り文字 で始まる、絶対パスなら基準の外
const outside =
  rel === ".." || rel.startsWith(".." + path.sep) || path.isAbsolute(rel);
if (outside) {
  // 拒否する
} else {
  fs.readFile(target, callback); // base 配下と確認できたので読み込む
}

rel.startsWith("..")だけで判定すると、..dataのような基準内の正当な名前まで誤って拒否してしまうため、".." + path.sepで始まるかと".."そのものかを分けて確認します。実在しないパスやシンボリックリンクの扱いは環境で差があるので、例外を捕捉して安全側に倒す実装にします。フレームワークが静的ファイル配信の仕組みを備えている場合は、自前でパスを組み立てるより、その安全な仕組みに載せるほうが取りこぼしを減らせます。

パストラバーサルはユーザー入力を信頼しすぎることで起きるため、入口での検証と出口での処理という一般原則も併せて押さえると、対策の位置づけが整理できます。入口検証と出口エスケープの役割分担は別記事で扱っています。

あわせて読みたい

入力バリデーションと出力エスケープの原則。入口で検証し、出口で文脈別にエスケープする

まとめ

パストラバーサルは、ユーザー入力がパスの構造に触れ、基準ディレクトリの外へ解決されることで成立します。防御の要点は、パスをユーザーに書かせない設計と、やむを得ず組む場合に正規化してから範囲を検証する二段構えです。IPAの「安全なウェブサイトの作り方」でも、ディレクトリトラバーサルは代表的な脆弱性として取り上げられています。

パストラバーサル対策チェックリスト

  • ユーザー入力からファイル名やパスを丸ごと受け取っていないか(IDや列挙値に置き換えられないか)
  • 受け取る値を許可リスト(候補や文字種の制限)で検証しているか
  • ../の除去だけに頼っていないか(拒否リストを主対策にしていないか)
  • パスを組む場合、正規化してから基準ディレクトリ配下か検証しているか
  • 検証の順序が正しいか(正規化した後のパスを基準と突き合わせているか)
  • シンボリックリンクの解決先まで基準内か確認しているか
  • アプリのプロセス権限を、必要なファイルの範囲に最小化しているか

出典・参考

この記事をシェア

関連する記事

セキュアコーディング

入力バリデーションと出力エスケープの原則。入口で検証し、出口で文脈別にエスケープする

セキュアコーディングの土台である入力バリデーションと出力エスケープを、それぞれの役割の違いから整理します。入口の検証は防御の一段目、出口の文脈別エスケープがXSSやインジェクションの根本対策である理由を、原理と実務の判断基準まで掘り下げます。