cmd.exeのせいで子プロセス呼び出しが脆弱になっちゃう件

# TL;DR

- cmd.exeはカレントディレクトリーにあるファイルを`PATH`にあるファイルより優先して実行してしまうと言うクソ仕様なので気をつけよう。
- 安全のために、[vim-gitgutter](https://github.com/airblade/vim-gitgutter)を使っている人は以下の設定を追記しよう。

    ```vim
    if has('win32')
      " Git for Windowsをデフォルトでインストールした場合。お使いの環境に合わせて変えること。
      let g:gitgutter_git_executable = 'C:\Program Files\Git\bin\git.exe'
    endif
    ```
- 当然、ほかにも危険なVimプラグイン（など）がたくさんあると想定される。ほかのコマンドを呼び出すソフトウェアを書く際は、少なくともWindowsで使用されることを想定する場合は、極力shell経由で呼び出すAPIは使用しないこと。自動でcmd.exeが使用され、脆弱性になり得る。

# 事例

とあるJavaScriptが中心のリポジトリーをvimで探索していたところ、
いきなりWindows Script Host (以下WSH)のエラーが。

![WSHのエラー](https://qiita-image-store.s3.amazonaws.com/0/12688/d8276656-dac3-4857-0f79-c9047cc1a03d.png)

しかもこれ、OKを押しても少したつとまた出てくる。
まともに編集できたじゃない。

どうしたものかと思ってタスクマネージャーを見ると、確かに`gvim.exe`から大量にWSHが実行されていることがわかる。
タスクマネージャー曰くどうやら`gvim.exe`がなぜか`git.js`を起動し、そこからWSHが起動しているらしい。

さらに`gvim.exe`から生えている`git.js`に渡された引数から推測するに、どうやら[gitgutter](https://github.com/airblade/vim-gitgutter)というVimプラグインから呼び出されたものらしい。
gitgutterのIssueやソースを漁るも、これといった原因とおぼしきものは見つからない。
単にgitgutterは単に`cmd.exe`を介して`git`を呼んでいるだけなのだ。

これ以上掘るところも思いつかず、どうしようかと頭をひねってみたら、すぐに気づいた。
問題のディレクトリーに、`git.js`なるファイルがあったのだ。
gitgutterはどうやら`git`コマンドを実行するつもりで、間違ってその`git.js`をJScriptとして実行し、JScriptの処理系であるWSHを起動してしまったのである。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">原因わかった。git.jsっていうファイルがカレントディレクトリーにあるからgitコマンドを実行したつもりがgit.jsを開いていた、ただそれだけのことか。<br>Windowsのクソ仕様じゃん。これ、ともすれば悪用できるでしょ。。。<br>サブコマンドを実行するもろもろの多くが影響受けるじゃん。。。</p>&mdash; Yuji Yamamoto: 山本悠滋 (@igrep) <a href="https://twitter.com/igrep/status/984673940436992000?ref_src=twsrc%5Etfw">2018年4月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 原因

cmd.exe, またの名をコマンドプロンプトは、カレントディレクトリーにあるファイルを**`PATH`環境変数に列挙されているパスよりも優先して探索する**というひどい仕様となっている。
今回のgitgutterのように自動的に実行されるコマンドと組み合わされてしまった場合、

- 呼び出されるコマンドと同じ名前で、
- 拡張子が環境変数`PATHEXT`で列挙されているもの（デフォルトでは`.js`や`.bat`, `.exe`など）

を、カレントディレクトリーに置いただけで意図しないファイルを実行できてしまう。

加えてgitgutterはその性格上、`git`コマンドを何度も呼び出す必要があるため、何度WSHのダイアログを閉じてもエラーを発生させてしまう。
`git.js`というファイルをリポジトリーに置いて開かせるだけでvimを使い物にならない状態にできてしまうのだ。

# とりあえずの対策

冒頭にも挙げたが、今回の問題はカレントディレクトリーにある`git`という名前のファイルを呼んでしまうのが問題なので、必ず本物の`git`コマンドを参照するよう設定すればよい。
vimrcに下記のように書けば実現できる。

```vim
if has('win32')
  " Git for Windowsをデフォルトでインストールした場合。お使いの環境に合わせて変えること。
  let g:gitgutter_git_executable = 'C:\Program Files\Git\bin\git.exe'
endif
```

これをユーザー全員が行うのも微妙なので、一応[gitgutterのIssue](https://github.com/airblade/vim-gitgutter/issues/508)として報告したものの、直してもらえるかは微妙だ。

# もっと根本的な対策

いずれにせよ、この問題はcmd.exeを何らかの形で使用している以上避けることができない。
gitgutterだけでなく、同じように子プロセスを呼ぶVimプラグイン（や、その他のプログラム）は、cmd.exeを介して呼ぶ限り、間違ってカレントディレクトリーにあるファイルを実行してしまうリスクを伴う。
そして困ったことに、 `:!` コマンドや`system`関数など、vimにおける子プロセスを呼び出す機能の多くはデフォルトで標準のシェル、つまりWindowsでは`cmd.exe`を介してコマンドを実行するよう作られている[^msys2]。シェルのメタキャラクターを解釈したり、コマンドラインをパースする必要があるためだ。

[^msys2]: vim以外のサンプルを示すために手元のgccやperlを試してみたものの、再現しなかった。恐らくMSYS2でインストールしたため、MSYS2のデフォルトシェル（普通はbash）が設定されたのだろう。残念ながら、[Haskellの`callCommand`関数](http://hackage.haskell.org/package/process-1.4.1.0/docs/System-Process.html#g:2)はダメだった。手を抜きたいとき以外は、代わりに`callProcess`関数を使うこと。[typed-processパッケージの`shell`関数](http://hackage.haskell.org/package/typed-process-0.2.2.0/docs/System-Process-Typed.html#v:shell)も同様。

これを根本的に防ぐには、プラグインの開発者が`cmd.exe`を使用しないよう、別の手段を使うしかない。
この点に考慮したのか、幸い、vimの`job_start`関数やNeovimの`jobstart`関数では、第1引数をリストとして渡せば、cmd.exeを介さず、直接指定したコマンドを呼び出すことができる。
この方法はよくあるOSコマンドインジェクションを防ぐことにもつながるので、積極的に使うべきだろう。
残念ながら、vimの`system`関数や`:!`コマンドはこの呼び方をサポートしておらず、必ずcmd.exeを介して呼んでしまう（Neovimの`system`関数はサポートしている）。
Vim scriptの中で気軽に`system`関数や`:!`を呼ぶのはやめた方がいいだろう。

また、今回は試していないが、どうしてもシェルの特殊な機能が使いたいという場合は、PowerShellを介して呼ぶこともできる。
Microsoftも反省したのか、`powershell.exe`には今回紹介したような危険な仕様はない。
vimの設定項目`shell`として`powershell.exe`を指定すれば、vimプラグインの開発者だけでなく、ユーザー自らが防ぐこともできる（はず）。

# ちなみに

先ほどの事例は`.js`ファイルに対する関連付けがデフォルトのWSHとなっていた場合に起こるものだが、
もし`gvim.exe`を設定していた場合、`git.js`を開いた瞬間無限にgvim.exeが立ち上がり、今度はvimどころかWindowsが使い物にならない状態になってしまう :scream:。

GVimがgitgutterを使用して`git`を呼ぶ -\> `git.js` が立ち上がる -\> 関連付けられている`gvim.exe`が立ち上がる -\> ...
という無限再帰呼び出しが起こるためだ。

間違ってこれをしてしまって私は何度か電源長押しをせざるを得なくなってしまった :innocent:
それでもちっとも狂わない最近のWindowsはすごいなぁと今更ながら思う。