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風入力も先に弾くと、事故を減らせます。