47293


Windows10において、`mydir`という名前のディレクトリ、`myfile.txt` というファイルが存在するとき、Javaの

[code]

    Files.exists(Paths.get("mydir.")); // 1個の末尾ドットが無視される？(2個以上はfalse)
    Files.exists(Paths.get("myfile.txt...")); // 1個以上の末尾ドットが無視される？
    
[/code]

が真となるのですが、なぜこのような挙動になるのでしょうか。  
また、正しく(つまり`mydir.`,`myfile.txt.`は存在しない、と)判断できるようにするためにはどうすべきでしょうか。

ちなみに、C#で試したところ

[code]

    Directory.Exists("mydir...."); // 1個以上
    File.Exists("myfile.txt.........."); // 1個以上
    
[/code]

が真となりました。(ディレクトリ存在の判断が異なるのも少し気になります…)

Java/C#ともCentOS7上では偽となっていました。

* * *

[以下補足情報]

Webサーバに対する[ディレクトリトラバーサル](https://ja.wikipedia.org/wiki/%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%83%88%E3%83%A9%E3%83%90%E3%83%BC%E3%82%B5%E3%83%AB)攻撃というのは、一般的には親方向にトラバースさせると思いますが、この挙動を利用すると子方向へのトラバース可能性も出てくるかと思います。

例えば、Java Servlet仕様([JSR 369: Java Servlet 4.0
Specification](https://www.jcp.org/en/jsr/detail?id=369) 10.5 Directory
Structure)によると `/WEB-INF` ディレクトリ以下への直接のリクエストは拒否されるべきですが、後ろにドットを付け、
<http://example.com/WEB-INF./web.xml> (あるいは小文字で <http://example.com/web-
inf./web.xml>
)とすることで該当ディレクトリ以下のファイルが漏洩する脆弱性が過去実際にあったようです([Tomcat](https://coderanch.com/t/547261/application-
servers/WEB-INF-web-xml-exposed),
[WildFly](https://jvndb.jvn.jp/ja/contents/2016/JVNDB-2016-001933.html))。


CC BY-SA 4.0
2018-08-06T22:20:45.400
47292
2018-08-08T22:32:48.870
2018-08-07T00:55:46.517
2808
2808
question
windows
Windowsでファイル名の末尾ドットが無視される挙動の由来


## Windowsでファイル名の末尾ドットが無視される挙動の由来

Windowsが当初使用していたFATファイルシステムの影響です。

[FATファイルシステムのディレクトリエントリ](https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system#Directory_entry)の構造としては、ファイル名８バイト、拡張子３バイトの固定長フィールドとなっており、それぞれの長さに満たない場合は空白で埋められる仕様です。このため、ファイル名と拡張子の間に`.`は格納されていません。  
例えば空白を`_`で表すと、ファイル名`README.TXT`であれば`README__TXT`と格納され、`README`であれば`README_____`となります。

このような事情があるため、Windows
APIでは暗黙の`.`を許容します。つまり`README.`、`README.*`、`*.*`などで検索を行うとファイル名`README`がヒットします。現在はNTFS等、他のファイルシステムが使われていますが、ファイルシステムに依らずWindows
APIは上記振る舞いを継承しています。

## ではどうすべきか

### 実は使うべきでない、使えない

Windowsのファイル名について説明するドキュメント[Naming Files, Paths, and
Namespaces](https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-
file)の[Naming Conventions](https://docs.microsoft.com/en-
us/windows/desktop/FileIO/naming-a-file#naming-conventions)では

>   * Do not end a file or directory name with a space or a period. Although
> the underlying file system may support such names, the Windows shell and
> user interface does not.
>

と、たとえファイルシステムとして`.`で終わるファイル名が認められているとしても、利用者としてはそのようなファイル名は使用するな、シェル（つまりファイルエクスプローラー）やアプリケーションがサポートしていない可能性がある、としています。

末尾ドットや末尾空白を無視するこの処理はファイルが見つからなかった際のフォールバック処理ではなく、ファイル操作前のパスの正規化の一環です（`./`や`../`と同じ扱いです）。  
例えば`README.`が存在する状況で`README.`を開こうとしても`README`が存在しないというエラーが発生します。

yukihaneさんの回答で触れられていた[`GetFullPathNameW`](https://docs.microsoft.com/en-
us/windows/desktop/api/fileapi/nf-fileapi-
getfullpathnamew)やより目的に近い[`PathCanonicalizeW`](https://docs.microsoft.com/en-
us/windows/desktop/api/shlwapi/nf-shlwapi-
pathcanonicalizew)においても同じことで、ファイルの存在とは無関係にパスの正規化として無条件に末尾`.`を削除します。

### それでも使いたい場合

質問文はファイルの存在確認だけを対象としています。存在確認だけに有効な方法としては、ディレクトリ内のファイル一覧を取得し、自分でファイル名一致の判定を行うことです。

ただし、前述の通り、ファイルが存在が確認できたとしてもアクセスできません。

## それでもそれでも使いたい場合

Windowsには正規化を行わない`\\\?\`プレフィックスというパス形式が存在します。例えばメモ帳であれば`\\\?\C:\Windows\notepad.exe`と表現されます。この形式の特徴は次の通りです。

  * 相対パスは認められません。常に絶対パスを使用する必要があります。
  * Unicodeバージョンではパス文字列の最大長が260文字から32,767文字へ拡張されます。
  * ディレクトリ区切りに`/`を使用できません。ディレクトリ区切りは`\`を使用する必要があります。
  * `..\`や`.\`は認められません。`..`や`.`を正当なファイル名として扱います。
  * 末尾`.`や末尾``を無視しません。正当なファイル名として扱います。

`\\\?\`プレフィックスを付けても次の動作は引き続き有効です。

  * 大文字・小文字を無視します。
  * short file nameを受け入れます。

この`\\\?\`プレフィックス形式を使用すれば、ファイル名の末尾ドットについて指定通り正しく扱うことができます。

ただし、これはあくまでWindows
APIでの話です。例えばC#では`\\\?\`形式のパスを認めていないため常に失敗します。C#で`\\\?\`形式を使用するためには直接Windows
APIを呼び出す必要があります。質問文で触れられているもう一つの言語Javaについては私は可否を知りません。


CC BY-SA 4.0
2018-08-06T23:53:37.583
47293
2018-08-08T22:32:48.870
2018-08-08T22:32:48.870
4236
4236
47292
answer


[追記]  
React OSの `CreateFile` ソースを眺めていたところ、どうやらこの辺りのパス操作を行っているのは
[`RtlDosPathNameToNtPathName_U`](https://github.com/mirror/reactos/blob/24ae5dce7fe56b254b4d5c7952e674490f50829f/reactos/lib/rtl/path.c#L398)
という関数のようです。(実際のWindows APIとしても存在するようだが、ドキュメント化されていない)

この名前で検索してみたところ、次のblogエントリに本件に関わるWindowsパスについてよくまとまっているように見えます(下記の私の回答よりよっぽど正確かと思われます…):

  * [Project Zero: The Definitive Guide on Win32 to NT Path Conversion](https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html)

[追記終わり]

* * *

質問に記載したような挙動となっている理由は自分にはわかりそうもなかったので、代わりに現状の挙動を確認してみることにしました。

  * 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)`](http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java#l321)に突き当ります。  
基本的にはここの`WindowsChannelFactory.newFileChannel(...)`でチェックを行っているようです。  
さらに進むと最終的には[`WindowsNativeDispatcher.CreateFile(...)`](http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/windows/classes/sun/nio/fs/WindowsNativeDispatcher.java#l50)、[`CreateFileW`関数](http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/windows/native/sun/nio/fs/WindowsNativeDispatcher.c#l242)でチェックしていました。

ただしディレクトリの場合はここでは判断せず([`FILE_FLAG_BACKUP_SEMANTICS`フラグ](https://msdn.microsoft.com/ja-
jp/library/cc429198.aspx)を用いず)、  
[`WindowsDirectoryStream`コンストラクタが例外を出さない](http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java#l335)ことを以て存在確認を行っています。  
こちらは[`FindFirstFileW`関数](http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/windows/classes/sun/nio/fs/WindowsDirectoryStream.java#l78)でチェックしています。  
このとき、ディレクトリ名として入力した名称をそのまま用いず、末尾に[`"\\\\*"`](http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/windows/classes/sun/nio/fs/WindowsDirectoryStream.java#l75)を付与します。  
例えば`mydir..`と入力したなら、`FindFirstFile`へ渡すファイル名は
`"mydir..\\\\*"`となります。(※実際には絶対パスで渡される)

* * *

次に、上記で登場したWindows API `CreateFileW`,
`FindFirstFileW`関数がファイル名の末尾ドットをどう扱っているのか調べてみました。  
結果、調べた範囲ではどちらもドットの扱いは同じように見え、次のような挙動に見えました:

  * パスの最後のファイル(ディレクトリも同様)の末尾ドットは2つ以上付与しても無視される
  * パスの途中に登場するディレクトリの末尾ドットは1つなら無視されるが2つ以上はNG

具体例としては、ディレクトリ`mydir`とその直下にテキストファイル`mydir/myfile.txt`が存在する場合、次のような挙動となります:

[code]

     // 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);
    
[/code]

質問文中にある、JavaとC#で`mydir..`をチェックした時の挙動が異なる件については、これが理由かと思います。つまり、C#では入力ファイル名を素直にチェックしているが、Javaでは前述の通り`mydir..\\\\*`をチェックするためパス途中にドットが2個存在する状態になり存在チェックNGになります。  
(余談ですが、Javaのファイル存在チェックでも、`new File("mydir..").exists()`の方は`true`を返します。)

* * *

ここまでの結果より、質問に記載した挙動は、JavaやC#といった言語特有の事象でなく、Windows環境一般に言える(おそらく他の言語でも発生しうる)事象であると考えられます。

* * *

続いて対処法ですが、Windows APIでは[`GetFullPathNameW`](https://docs.microsoft.com/en-
us/windows/desktop/api/fileapi/nf-fileapi-getfullpathnamew)が利用できそうに思われます。  
第3引数`lpBuffer`に前述のようなルールでドットを無視した後のパスが返ってくるようなので、自分が指定したパスそのものなのかドットが削られたのかが判断できます。

[code]

    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個は無視されないので入力文字列と(後方)一致する
    
[/code]

Javaでは[`Path#toRealPath()`](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/Path.html#toRealPath-
java.nio.file.LinkOption...-)メソッドが似たような挙動を示すようです。  
ただし、このメソッドは、[パスセパレータごと区切って1階層ずつドットの処理(等)を行っている](http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/windows/classes/sun/nio/fs/WindowsLinkSupport.java#l229)ため、前述`GetFullPathName`の挙動と完全には一致しません。

[code]

    Paths.get("mydir....\\myfile.txt..").toRealPath().toString();
    // F:\programs\so47292\mydir\myfile.txt
    // 結果的に、途中の2個以上ドットも無視される
    
[/code]

質問文中にある[WildFly脆弱性の対応で修正されたメソッド](https://github.com/undertow-
io/undertow/commit/b1df3310f50a7cf45160194a18f15dc4ddc623bb#diff-27646595bdb8fc0110b6fadc75016436L245)は、(この対応後も修正が為され、現時点最新版では)この`Path#toRealPath()`と[`Path#normalize()`](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/Path.html#normalize--)の[結果を比較して等しいかどうか](https://github.com/undertow-
io/undertow/blob/383d29887450ff41c3bdd382b2560d835d6bf191/core/src/main/java/io/undertow/server/handlers/resource/PathResourceManager.java#L317)、というチェックを行っています。

* * *

[IPA ISEC セキュア・プログラミング講座 8-1.
Windowsパス名の落とし穴](https://www.ipa.go.jp/security/awareness/vendor/programmingv1/b08_01.html)に本件に関連する話題が記載されていました。  
末尾ドット以外にも多くの考慮すべき事項があるようです。  
(参考文献リンクも有用そうなものが並んでいますが、残念ながら全てリンク切れのようです…)

末尾ドットを無視することを指してこのページでは「サービス機能」と称していますが、「サービス機能の不活性化」節で記載されている

> パス名の先頭に\\?\という４文字を付け加える

も利用できるかもしれません。


CC BY-SA 4.0
2018-08-07T13:56:55.637
47326
2018-08-08T05:43:57.907
2018-08-08T05:43:57.907
2808
2808
47292
answer