12k
All articles

Node.jsにおけるパストラバーサル攻撃の防止

Node.jsのpath traversal対策を解説。path.resolveとpath.sepでの範囲確認、ユーザー入力ではなくIDベースのファイル取得を紹介。

OpenReplay Team
OpenReplay Team
Node.jsにおけるパストラバーサル攻撃の防止

Expressアプリケーションがユーザー入力に基づいてファイルを提供する場合—ダウンロードルート、アップロードプレビュー、動的アセットなど—パストラバーサルの脆弱性が潜在的に存在します。これはNode.jsにおける最も一般的で深刻なファイルセキュリティの問題の一つであり、気づかないうちに導入してしまうことが驚くほど簡単です。

本記事では、パストラバーサル攻撃の仕組み、一般的な「修正方法」が不十分な理由、そしてNode.jsにおける安全なファイル処理が実際にどのようなものかを解説します。

重要なポイント

  • パストラバーサル攻撃は、ユーザーが提供したファイルパスの検証が不足または不完全であることを悪用し、攻撃者が意図したディレクトリ外のファイルにアクセスできるようにします。
  • path.normalize()path.join()はセキュリティツールではありません—パスを整形しますが、ディレクトリトラバーサルを防ぐことはできません。
  • 正しい防御策は、path.resolve()でパスを解決し、結果が許可されたベースディレクトリ内に留まることをpath.sepを含むstartsWithチェックで検証することです。
  • 最も安全なアプローチは、ユーザーが提供するパスを完全に避け、IDベースのルックアップを通じて入力をファイルにマッピングすることです。

パストラバーサル攻撃とは?

パストラバーサル攻撃(ディレクトリトラバーサルとも呼ばれます)は、攻撃者がファイルパス入力を操作して、意図したディレクトリ外のファイルにアクセスする際に発生します。典型的な例:

GET /download?file=../../etc/passwd

サーバーがこの入力を単純にベースディレクトリと結合してファイルを読み取る場合、システムのパスワードファイルを渡してしまうことになります。

この攻撃は高度なものではありません。入力検証の欠如または不完全性を悪用します—具体的には、解決されたパスが許可されたディレクトリ内に留まることを検証しない失敗です。

path.normalize()path.join()が不十分な理由

Node.jsのパストラバーサル防止について理解すべき最も重要なことは:これらのユーティリティはセキュリティツールではないということです。

path.normalize()..シーケンスを解決し、冗長なスラッシュを整理します。path.join()はセグメントを連結します。どちらもトラバーサルを防ぐことはできません—意図したディレクトリの外を指す可能性があるパスをより綺麗に生成するだけです。

次の一般的だが安全でないパターンを考えてみましょう:

// ❌ 安全でない: path.join はトラバーサルを防げない
app.get('/download', (req, res) => {
  const filePath = path.join(__dirname, 'uploads', req.query.file)
  res.sendFile(filePath)
})

req.query.file../../etc/passwdの場合、path.join()はそれを綺麗に解決し、誤ったファイルを送信します。

URLエンコーディングはさらに問題を悪化させます。攻撃者は..%2F..%2Fetc%2Fpasswdを送信するかもしれません。ExpressはURLパラメータを自動的にデコードするため、受け取ったデコード済みの値を検証する必要があります。生のURLを自分で処理している場合は、検証前に一度(適切なエラーハンドリングを伴って)デコードしてください。

正しいパターン: 解決と封じ込めの検証

Node.jsにおける安全なファイル処理の信頼できるアプローチは:

  1. path.resolve()を使用して、ユーザーが提供したパスをベースディレクトリに対して解決する。
  2. 解決されたパスがそのディレクトリ内に留まることを検証する。
const path = require('path')

const BASE_DIR = path.resolve(__dirname, 'uploads')

function safeResolve(userInput) {
  const resolved = path.resolve(BASE_DIR, userInput)

  // 解決されたパスがBASE_DIR内にあることを確認
  if (!resolved.startsWith(BASE_DIR + path.sep) && resolved !== BASE_DIR) {
    return null
  }

  return resolved
}

// ✅ 安全なExpressファイルダウンロードルート
app.get('/download', (req, res) => {
  const safePath = safeResolve(req.query.file)

  if (!safePath) {
    return res.status(403).send('Access denied')
  }

  res.sendFile(safePath)
})

path.sepの追加に注意してください。これがないと、ベースディレクトリが/uploadsの場合、/uploads-other/secret.txtが誤ってプレフィックスチェックを通過してしまいます。

Windows環境ではpath.resolve()path.sepがバックスラッシュを正しく処理するため、このパターンはクロスプラットフォームで機能します。より厳密な検証のために、path.relative()を使用してパスを比較し、ベースディレクトリから逸脱する結果を拒否することもできます。

シンボリックリンクは依然としてベースディレクトリの外を指す可能性があることに注意してください。機密ファイルを提供する場合は、送信前にfs.realpath()で実際のパスを解決してください。

さらに良い方法: ユーザーが提供するパスを完全に回避する

最も安全なアプローチは、ユーザーが提供するパスを一切使用しないことです。代わりに、ユーザー入力を間接的にファイルにマッピングします:

// ✅ IDベースのファイルルックアップ — ユーザー入力からのパス構築なし
const FILES = {
  'report-2024': 'reports/annual-2024.pdf',
  'invoice-001': 'invoices/inv-001.pdf',
}

app.get('/download/:id', (req, res) => {
  const filePath = FILES[req.params.id]

  if (!filePath) {
    return res.status(404).send('Not found')
  }

  res.sendFile(path.resolve(__dirname, 'secure', filePath))
})

これによりトラバーサルのリスクが完全に排除されます。ユースケースが許す場合—ファイルダウンロード、ドキュメントエクスポート、ユーザー固有のアセットなど—これが最初に選択すべきパターンです。

Express静的ファイル配信に関する注意

express.static()は、定義されたルートディレクトリへのアクセスを制限するため、公開アセットの提供には一般的により安全です。リクエストパラメータからパスを構築したり、意図しないディレクトリを公開したりするカスタムファイル配信ルートを記述する際にリスクが現れます。設定とルートの選択は依然として重要です。

まとめ

パストラバーサルは、明確な脆弱性であり、明確な修正方法があります:ユーザー入力をファイルパスとして決して信頼しないこと。固定されたベースに対して解決し、path.resolve()path.sepを含むstartsWithチェックで封じ込めを検証し、使用前にデコードされた入力を検証してください。可能な限り、ユーザー入力からのパス構築を回避するIDベースのルックアップを優先してください。path.normalize()path.join()は便利なユーティリティですが、セキュリティの保証ではありません。

よくある質問

express.static()は単独でパストラバーサルから保護しますか?

定義されたルートディレクトリからファイルを提供し、内部的にパス解決を処理することで役立ちますが、完全な安全性を保証するものではありません。設定ミスや機密ディレクトリの公開は依然としてリスクを生み出す可能性があります。ユーザー入力を使用するカスタムファイル配信ルートが、ほとんどの脆弱性が発生する場所です。

ユーザー入力でドットドットシーケンスをチェックすることは信頼できる防御策ですか?

いいえ。ドットドットシーケンスを含む文字列をフィルタリングまたは拒否することは脆弱です。攻撃者はURLエンコーディング、二重エンコーディング、またはプラットフォーム固有のパスセパレータを使用してそのようなチェックをバイパスできます。信頼できる防御策は、path.resolve()で完全なパスを解決し、その後startsWithチェックまたはpath.relative()を使用して、結果が意図したベースディレクトリ内に収まることを検証することです。

このパストラバーサル防止パターンはWindowsで機能しますか?

はい。path.resolve()とpath.sepを使用することで、封じ込めチェックがクロスプラットフォームで機能します。path.resolve()はWindowsパスを正規化し、path.sepは正しいセパレータに評価されます。より厳密な検証のために、path.relative()を使用して、解決されたパスがベースディレクトリから逸脱しないことを確認できます。

ファイルを送信する前に、ファイルが存在することも確認すべきですか?

はい。解決されたパスがベースディレクトリ内にあることを確認した後、res.sendFile()を呼び出す前にfs.existsSync()またはfs.access()を使用してファイルが存在することを検証してください。これにより、エラーメッセージを通じてディレクトリ構造に関する情報が漏洩することを防ぎ、ファイルが見つからない場合のレスポンスを制御できます。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.