パストラバーサル

パストラバーサル

説明

パストラバーサルは、攻撃者がファイルパスに ../ などを含め、許可されていないファイルやディレクトリにアクセスする脆弱性です。主に、未検証の入力をファイルシステムの関数に渡すと発生し、重要なシステムファイルや機密情報の読み取り・上書きにつながるおそれがあります。

想定される影響

  • 重要なシステムファイルや、ほかのユーザーのデータへの不正アクセス
  • 重要なファイルの改ざんや削除
  • 必須ファイルの破損によるサービス障害

対処方法

  • 入力に ../、/、\ などのパスを操作する文字列がないか確認してください。
  • ファイルアクセスを指定のディレクトリ内に制限し、絶対パスの使用を制限してください。
  • Node.jsの path.normalize() などで正規化した後、結果が基準ディレクトリ内に収まることを検証してください。
  • 許可したファイル名や、あらかじめ定めた名前の規則だけを認めてください。
  • 第1引数がパスの fs.writeFile(path, data) と、ファイル記述子の fs.write(fd, data) を区別してください。入力をファイル内容やファイル記述子に続くデータ引数にだけ使う場合は、パストラバーサルとは別に評価します。

例

変更前

javascript
const fs = require('fs');
const express = require('express');
const app = express();

app.get('/download', (req, res) => {
  // 変更前: 入力をそのままファイルシステムの呼び出しに使います。
  const fileName = req.query.file;
  fs.readFile(fileName, (err, data) => {
    if (err) return res.status(404).send('File not found');
    res.send(data);
  });
});

変更後

javascript
const fs = require('fs');
const path = require('path');
const express = require('express');
const app = express();

// 検証から読み取りまでのリンク置換を防ぐため、サーバー以外は書き込めないようにします。
const BASE_DIR = fs.realpathSync(path.join(__dirname, 'files'));

function isInside(baseDir, targetPath) {
  const relative = path.relative(baseDir, targetPath);
  return (
    relative !== '' &&
    relative !== '..' &&
    !relative.startsWith('..' + path.sep) &&
    !path.isAbsolute(relative)
  );
}

app.get('/download', (req, res) => {
  const rawName = req.query.file;
  if (typeof rawName !== 'string') {
    return res.status(400).send('Invalid file path');
  }

  // ファイル名だけを許可し、ディレクトリ要素は取り除かずに拒否します。
  const fileName = path.basename(rawName);
  if (fileName !== rawName || !/^[a-zA-Z0-9._-]+$/.test(fileName)) {
    return res.status(400).send('Invalid file path');
  }

  const candidate = path.resolve(BASE_DIR, fileName);
  if (!isInside(BASE_DIR, candidate)) {
    return res.status(400).send('Invalid file path');
  }

  let requestedPath;
  try {
    requestedPath = fs.realpathSync(candidate);
  } catch {
    return res.status(404).send('File not found');
  }
  if (!isInside(BASE_DIR, requestedPath)) {
    return res.status(400).send('Invalid file path');
  }

  fs.readFile(requestedPath, (err, data) => {
    if (err) return res.status(404).send('File not found');
    res.send(data);
  });
});

app.post('/message', (req, res) => {
  // パスはサーバーで固定し、入力はファイル内容にだけ使います。
  fs.writeFile(path.join(BASE_DIR, 'message.txt'), req.body.message, (err) => {
    if (err) return res.status(500).send('write failed');
    res.send('saved');
  });
});

説明:

  • 変更前: fileName を検証せずに fs.readFile に渡すため、../ などでパスを操作できます。プロセスに読み取り権限のあるファイルにアクセスされるおそれがあります。
  • 変更後: 文字列型と path.basename() との一致を確認し、ディレクトリ要素を拒否します。正規化したパスと、シンボリックリンクを解決した実際のパスが、どちらもサーバー管理の BASE_DIR 内にあることを確認します。サーバーがパスを固定し、入力をファイル内容にだけ使う場合は、パストラバーサルとは別の問題として扱います。

POSTの例は、リクエスト本文を解析するミドルウェアを設定済みと仮定しています。

参考資料