← ./articles-ja

Node.jsでWindowsパスの範囲チェックをstartsWithで書かない

ユーザー指定のファイルパスが、許可したroot配下にあるかを確認したいことがあります。

つい次のように書きたくなります。

filePath.startsWith(rootPath)

しかしWindowsでは危険です。drive-relative path、UNC path、device path、大小文字、.. traversalなどがあり、文字列prefixだけでは正しく判定できません。

安全な基本形

path.resolve()path.relative() を使います。

const path = require("path");

function isSafeInRoot(filePath, rootPath) {
  if (typeof filePath !== "string" || typeof rootPath !== "string") {
    return false;
  }

  if (/^[a-zA-Z]:[^/\\]/.test(filePath)) {
    return false; // C:foo のようなdrive-relative
  }

  if (/^\\\\/.test(filePath) || /^\/\//.test(filePath)) {
    return false; // UNCやdevice風の入力
  }

  const resolvedFile = path.resolve(filePath);
  const resolvedRoot = path.resolve(rootPath);
  const relative = path.relative(resolvedRoot, resolvedFile);

  return relative !== "" &&
    !relative.startsWith("..") &&
    !path.isAbsolute(relative);
}

root自身を許可したい場合は relative === "" の扱いを変えてください。upload先や保存先チェックでは、root自身を拒否したほうが単純なこともあります。

startsWithが危ない理由

次の2つを考えます。

<root>\safe
<root>\safe-old

2つ目は文字列としては <root>\safe で始まりますが、<root>\safe ディレクトリ配下ではありません。

さらにWindowsには次のような形があります。

<drive-relative>foo
\\server\share\file.txt
\\?\<root>\safe\file.txt
<root>\safe\..\secret.txt

生の文字列ではなく、正規化したパス同士で比較する必要があります。

入力境界で検証する

パス検証は、できるだけ入力に近い場所で行います。

  • HTTP upload先
  • CLI引数
  • IPC command
  • file picker結果
  • plugin API呼び出し

後段のfallbackで守るのではなく、許可root外の入力は最初に拒否します。

まとめ

Windowsでパスの範囲チェックをするなら、startsWith は避けてください。

path.resolve() で正規化し、path.relative() でrootから見た相対パスを取り、.. や絶対パスを拒否します。drive-relativeやUNC/device風入力も先に弾くと、事故を減らせます。

参考