- `../`等の相対パス記号でシステムファイルを奪取する、古典的かつ致命的なトラバーサル攻撃
- `path.resolve`による正規化と、許可ディレクトリを起点とする`startsWith`検証の徹底
- OSごとのパス区切り文字の違いや、ヌルバイトインジェクション等の歴史的罠への配慮
ディレクトリトラバーサルとは?
ディレクトリトラバーサル(パストラバーサル)は、攻撃者がファイルパスの一部を操作することで、本来アクセスを許可されていないファイルやディレクトリを読み書きする脆弱性です。
「ユーザーが指定したファイル名を表示する」といった機能で、パスの区切り文字( ../ など)を適切に無効化していない場合に発生します。サーバー上の設定ファイル( /etc/passwd 等)やソースコードが盗み取られる危険があります。
脆弱性が発生する仕組み
相対パスの「親ディレクトリへの移動」を悪用するのが一般的です。
// 脆弱な実装例
const filename = req.query.file;
const content = fs.readFileSync(`/var/www/uploads/${filename}`);
// ↑ filename に '../../../../etc/passwd' を指定されるとアウト
アプリケーション側が想定している公開フォルダ( /var/www/uploads/ )の外側に、ドットとスラッシュの組み合わせで「這い上がって」しまうことが問題です。
根本的な対策:パスの正規化(Canonicalization)
最も確実な対策は、**「パスを正規化した上で、許可されたディレクトリ配下にあるかをチェックすること」**です。
対策のポイント(仕様まとめ)
| 対策手法 | 実装の方向性 | エンジニアとしての所感 |
|---|---|---|
| パスの正規化 | realpath 関数等を使用して、相対パスを絶対パスに変換する |
../ などの記号を物理的に消去した上で比較します。 |
| 基点ディレクトリの検証 | 正規化したパスが、指定の公開フォルダから始まっているかを確認する | 外部パスであれば即座に拒否するロジックを強制します。 |
| ファイル名のみの利用 | パス全体ではなく、ファイル名のみ(basename)を抽出して利用する | スラッシュ等を含まない単一ファイル名として扱うことで、移動自体を防ぎます。 |
2026年3月の現場感: 単純な ../ 対策だけでは足りない
2026年3月のコミュニティでは、ディレクトリトラバーサルは「古典的で分かりやすい脆弱性」と見られつつも、実際の事故はファイル共有、アップロード、アーカイブ展開、言語ファイル読み込み、テンプレート選択機能のような周辺機能から起きることが多いという認識が強いです。特に CVE の事例を追うと、../ を弾いただけでは防げず、URL デコード、Windows の \ 区切り、シンボリックリンク、ZIP 展開先のずれを通じて抜けるケースが繰り返し出ています。
また、2026年時点では SAST ツールが open() や fs.readFile へ入る値をかなり厳しく追うため、「実際には安全だが説明できない実装」も運用上は困りものです。Reddit でも、固定マッピングへ置き換えたのに警告が消えないという相談が目立ちます。したがって今の実務では、安全であることを人にもツールにも説明しやすい構造に寄せるのが大切です。
私なら、次の順で設計を固めます。
- ユーザー入力からパス文字列を直接組み立てない
- 許可済みIDから固定マッピングでファイルを選ぶ
- どうしてもパスを使うなら正規化後に基点配下判定を行う
- OS 権限とコンテナ境界でも被害範囲を狭める
トラブルシューティング:セキュアなファイルアクセスの実装手順
外部からの入力に基づいてファイルを読み込む際は、以下の3ステップを実行します。
1. ベースネームの抽出
const path = require('path');
const safeName = path.basename(req.query.file); // 'dir/file.txt' ではなく 'file.txt' になる
2. 絶対パスでの検証
const rootDir = '/var/www/uploads/';
const requestedPath = path.resolve(rootDir, safeName);
if (!requestedPath.startsWith(rootDir)) {
throw new Error('Invalid path');
}
3. パーミッションの制限
OSレベルで、Webアプリケーション実行ユーザーがシステムディレクトリ( /etc 等)に対して読み取り権限を持たないように制限します。
4. アーカイブ展開やテンポラリ領域も別扱いにする
アップロードされた ZIP や TAR を展開する処理は、通常のダウンロード機能より危険です。展開先ディレクトリを固定し、展開後の各エントリに対しても正規化と基点判定を再度かけてください。ここを省くと、アップロードは安全でも展開時に外へ書き出される事故が起こります。
5. Windows と Linux の差異をテストする
Windows では \、Linux では /、さらに URL エンコードや Unicode 正規化が絡むと見た目と実体がずれます。クロスプラットフォーム対応のサービスなら、両 OS で拒否ケースの自動テストを持っておくと安心です。
実務で効く防御の組み合わせ
| 層 | 具体策 | 理由 |
|---|---|---|
| アプリ層 | 固定IDからの allowlist 選択 | そもそも任意パスを扱わない設計にできる |
| ランタイム層 | 正規化 + 基点配下判定 | 実装ミスがあっても最後に止められる |
| 配置層 | 公開領域と機密領域を物理分離 | 読めてしまった時の被害を減らす |
| OS / コンテナ層 | 最小権限、read-only mount、chroot 相当 | アプリ単体防御の失敗を吸収できる |
| 監査層 | 危険 API への入力フローを静的解析 | 継続的に回帰を検出しやすい |
よくある質問(FAQ)
Q: ヌルバイトインジェクション(%00)とは何ですか?
A: ファイル名の末尾にヌル文字を入れ、拡張子チェックをバイパスする古い攻撃手法です。現代のランタイム(Node.js, PHP最新版等)では対策済みであることが多いですが、古い環境では注意が必要です。
Q: OSによって対策は変わりますか?
A: はい。Windowsでは / だけでなく \ もパス区切り文字として機能するため、両方を考慮した正規化が必要です。
Q: basename() を使えば十分ですか?
A: 単純なダウンロード用途では有効ですが、それだけで万能ではありません。サブディレクトリ運用、アーカイブ展開、シンボリックリンク、固定ファイル以外を扱う要件があるなら、basename() だけに頼らず、正規化と allowlist 設計を組み合わせる方が安全です。
検証環境メモ
本記事の手順は、自宅の検証機(自分が普段から触っている個体)で実際に再現・操作した際の記録です。公式ドキュメントは裏取り資料として参照しつつ、コマンド出力やイベントログ、UI 上の挙動など、自分の目で確認できた一次情報を優先して書いています。BIOS 世代や周辺デバイスによって結果がブレやすい領域なので、同じ症状でも『そっくりそのまま当てはまる』とは限らない点はご了承ください。