質問: Windows10において、`mydir`という名前のディレクトリ、`myfile.txt` というファイルが存在するとき、Javaの ``` Files.exists(Paths.get("mydir.")); // 1個の末尾ドットが無視される?(2個以上はfalse) Files.exists(Paths.get("myfile.txt...")); // 1個以上の末尾ドットが無視される? ``` が真となるのですが、なぜこのような挙動になるのでしょうか。 また、正しく(つまり`mydir.`,`myfile.txt.`は存在しない、と)判断できるようにするためにはどうすべきでしょうか。 ちなみに、C#で試したところ ``` Directory.Exists("mydir...."); // 1個以上 File.Exists("myfile.txt.........."); // 1個以上 ``` が真となりました。(ディレクトリ存在の判断が異なるのも少し気になります...) Java/C#ともCentOS7上では偽となっていました。 --- [以下補足情報] Webサーバに対するディレクトリトラバーサル攻撃というのは、一般的には親方向にトラバースさせると思いますが、この挙動を利用すると子方向へのトラバース可能性も出てくるかと思います。 例えば、Java Servlet仕様(JSR 369: Java Servlet 4.0 Specification 10.5 Directory Structure)によると `/WEB-INF` ディレクトリ以下への直接のリクエストは拒否されるべきですが、後ろにドットを付け、 (あるいは小文字で )とすることで該当ディレクトリ以下のファイルが漏洩する脆弱性が過去実際にあったようです(Tomcat, WildFly)。
回答: [追記] React OSの `CreateFile` ソースを眺めていたところ、どうやらこの辺りのパス操作を行っているのは `RtlDosPathNameToNtPathName_U` という関数のようです。(実際のWindows APIとしても存在するようだが、ドキュメント化されていない) この名前で検索してみたところ、次のblogエントリに本件に関わるWindowsパスについてよくまとまっているように見えます(下記の私の回答よりよっぽど正確かと思われます...): * Project Zero: The Definitive Guide on Win32 to NT Path Conversion [追記終わり] 質問に記載したような挙動となっている理由は自分にはわかりそうもなかったので、代わりに現状の挙動を確認してみることにしました。 * Java8の挙動から辿って行ったので、Java固有の話と、Javaに依存しないWindows環境一般の話が混在しています * 今回、ジャンクション/シンボリックリンクについて考慮していませんが、それらを含めると話はさらに複雑化しそうです 今回下記コードを試した際のコンパイル、実行環境はOracle JDK 1.8.0_181, mingw-w64(i686-8.1.0-posix-dwarf-rt_v6-rev0), Windows10です。 まずはじめに、`Files.exists(Path)`メソッドの実装を調べてみました。このメソッドを下っていくと、`WindowsFileSystemProvider#checkReadAccess(WindowsPath)`に突き当ります。 基本的にはここの`WindowsChannelFactory.newFileChannel(...)`でチェックを行っているようです。 さらに進むと最終的には`WindowsNativeDispatcher.CreateFile(...)`、`CreateFileW`関数でチェックしていました。 ただしディレクトリの場合はここでは判断せず(`FILE_FLAG_BACKUP_SEMANTICS`フラグを用いず)、 `WindowsDirectoryStream`コンストラクタが例外を出さないことを以て存在確認を行っています。 こちらは`FindFirstFileW`関数でチェックしています。 このとき、ディレクトリ名として入力した名称をそのまま用いず、末尾に`"\\*"`を付与します。 例えば`mydir..`と入力したなら、`FindFirstFile`へ渡すファイル名は `"mydir..\\*"`となります。(※実際には絶対パスで渡される) 次に、上記で登場したWindows API `CreateFileW`, `FindFirstFileW`関数がファイル名の末尾ドットをどう扱っているのか調べてみました。 結果、調べた範囲ではどちらもドットの扱いは同じように見え、次のような挙動に見えました: * パスの最後のファイル(ディレクトリも同様)の末尾ドットは2つ以上付与しても無視される * パスの途中に登場するディレクトリの末尾ドットは1つなら無視されるが2つ以上はNG 具体例としては、ディレクトリ`mydir`とその直下にテキストファイル`mydir/myfile.txt`が存在する場合、次のような挙動となります: ``` // OK(パス末尾に2個以上。ディレクトリ。) CreateFile(__T("mydir.."), 0, 0, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); // OK(パス末尾に2個以上。ファイル。) CreateFile(__T("mydir\\myfile.txt..."), 0, 0, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); // OK(パス途中に1個) CreateFile(__T("mydir.\\myfile.txt"), 0, 0, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); // NG(パス途中に2個以上) CreateFile(__T("mydir..\\myfile.txt"), 0, 0, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); ``` 質問文中にある、JavaとC#で`mydir..`をチェックした時の挙動が異なる件については、これが理由かと思います。つまり、C#では入力ファイル名を素直にチェックしているが、Javaでは前述の通り`mydir..\\*`をチェックするためパス途中にドットが2個存在する状態になり存在チェックNGになります。 (余談ですが、Javaのファイル存在チェックでも、`new File("mydir..").exists()`の方は`true`を返します。) ここまでの結果より、質問に記載した挙動は、JavaやC#といった言語特有の事象でなく、Windows環境一般に言える(おそらく他の言語でも発生しうる)事象であると考えられます。 続いて対処法ですが、Windows APIでは`GetFullPathNameW`が利用できそうに思われます。 第3引数`lpBuffer`に前述のようなルールでドットを無視した後のパスが返ってくるようなので、自分が指定したパスそのものなのかドットが削られたのかが判断できます。 ``` GetFullPathName(__T("mydir\\myfile.txt"), MAX_PATH+1, lpBuffer, &lpFileName); // F:\programs\so47292\mydir\myfile.txt // 入力文字列と(後方)一致 GetFullPathName(__T("mydir.\\myfile.txt.."), MAX_PATH+1, lpBuffer, &lpFileName); // F:\programs\so47292\mydir\myfile.txt // 途中のドット1個や最後のドットが全部無視されるので一致しない GetFullPathName(__T("mydir..\\myfile.txt"), MAX_PATH+1, lpBuffer, &lpFileName); // F:\programs\so47292\mydir..\myfile.txt // 途中のドット2個は無視されないので入力文字列と(後方)一致する ``` Javaでは`Path#toRealPath()`メソッドが似たような挙動を示すようです。 ただし、このメソッドは、パスセパレータごと区切って1階層ずつドットの処理(等)を行っているため、前述`GetFullPathName`の挙動と完全には一致しません。 ``` Paths.get("mydir....\\myfile.txt..").toRealPath().toString(); // F:\programs\so47292\mydir\myfile.txt // 結果的に、途中の2個以上ドットも無視される ``` 質問文中にあるWildFly脆弱性の対応で修正されたメソッドは、(この対応後も修正が為され、現時点最新版では)この`Path#toRealPath()`と`Path#normalize()`の結果を比較して等しいかどうか、というチェックを行っています。 IPA ISEC セキュア・プログラミング講座 8-1. Windowsパス名の落とし穴に本件に関連する話題が記載されていました。 末尾ドット以外にも多くの考慮すべき事項があるようです。 (参考文献リンクも有用そうなものが並んでいますが、残念ながら全てリンク切れのようです...) 末尾ドットを無視することを指してこのページでは「サービス機能」と称していますが、「サービス機能の不活性化」節で記載されている > > パス名の先頭に\\?\という4文字を付け加える > > > も利用できるかもしれません。