TL;DR
-
cmd.exeはカレントディレクトリーにあるファイルを
PATH
にあるファイルより優先して実行してしまうと言うクソ仕様なので気をつけよう。 -
安全のために、vim-gitgutterを使っている人は以下の設定を追記しよう。
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)のエラーが。
しかもこれ、OKを押しても少したつとまた出てくる。
まともに編集できたじゃない。
どうしたものかと思ってタスクマネージャーを見ると、確かにgvim.exe
から大量にWSHが実行されていることがわかる。
タスクマネージャー曰くどうやらgvim.exe
がなぜかgit.js
を起動し、そこからWSHが起動しているらしい。
さらにgvim.exe
から生えているgit.js
に渡された引数から推測するに、どうやらgitgutterというVimプラグインから呼び出されたものらしい。
gitgutterのIssueやソースを漁るも、これといった原因とおぼしきものは見つからない。
単にgitgutterは単にcmd.exe
を介してgit
を呼んでいるだけなのだ。
これ以上掘るところも思いつかず、どうしようかと頭をひねってみたら、すぐに気づいた。
問題のディレクトリーに、git.js
なるファイルがあったのだ。
gitgutterはどうやらgit
コマンドを実行するつもりで、間違ってそのgit.js
をJScriptとして実行し、JScriptの処理系であるWSHを起動してしまったのである。
原因わかった。git.jsっていうファイルがカレントディレクトリーにあるからgitコマンドを実行したつもりがgit.jsを開いていた、ただそれだけのことか。
— Yuji Yamamoto: 山本悠滋 (@igrep) 2018年4月13日
Windowsのクソ仕様じゃん。これ、ともすれば悪用できるでしょ。。。
サブコマンドを実行するもろもろの多くが影響受けるじゃん。。。
原因
cmd.exe, またの名をコマンドプロンプトは、カレントディレクトリーにあるファイルを**PATH
環境変数に列挙されているパスよりも優先して探索する**というひどい仕様となっている。
今回のgitgutterのように自動的に実行されるコマンドと組み合わされてしまった場合、
- 呼び出されるコマンドと同じ名前で、
- 拡張子が環境変数
PATHEXT
で列挙されているもの(デフォルトでは.js
や.bat
,.exe
など)
を、カレントディレクトリーに置いただけで意図しないファイルを実行できてしまう。
加えてgitgutterはその性格上、git
コマンドを何度も呼び出す必要があるため、何度WSHのダイアログを閉じてもエラーを発生させてしまう。
git.js
というファイルをリポジトリーに置いて開かせるだけでvimを使い物にならない状態にできてしまうのだ。
とりあえずの対策
冒頭にも挙げたが、今回の問題はカレントディレクトリーにあるgit
という名前のファイルを呼んでしまうのが問題なので、必ず本物のgit
コマンドを参照するよう設定すればよい。
vimrcに下記のように書けば実現できる。
if has('win32')
" Git for Windowsをデフォルトでインストールした場合。お使いの環境に合わせて変えること。
let g:gitgutter_git_executable = 'C:\Program Files\Git\bin\git.exe'
endif
これをユーザー全員が行うのも微妙なので、一応gitgutterのIssueとして報告したものの、直してもらえるかは微妙だ。
もっと根本的な対策
いずれにせよ、この問題はcmd.exeを何らかの形で使用している以上避けることができない。
gitgutterだけでなく、同じように子プロセスを呼ぶVimプラグイン(や、その他のプログラム)は、cmd.exeを介して呼ぶ限り、間違ってカレントディレクトリーにあるファイルを実行してしまうリスクを伴う。
そして困ったことに、 :!
コマンドやsystem
関数など、vimにおける子プロセスを呼び出す機能の多くはデフォルトで標準のシェル、つまりWindowsではcmd.exe
を介してコマンドを実行するよう作られている1。シェルのメタキャラクターを解釈したり、コマンドラインをパースする必要があるためだ。
これを根本的に防ぐには、プラグインの開発者が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が使い物にならない状態になってしまう 。
GVimがgitgutterを使用してgit
を呼ぶ -> git.js
が立ち上がる -> 関連付けられているgvim.exe
が立ち上がる -> ...
という無限再帰呼び出しが起こるためだ。
間違ってこれをしてしまって私は何度か電源長押しをせざるを得なくなってしまった
それでもちっとも狂わない最近のWindowsはすごいなぁと今更ながら思う。
-
vim以外のサンプルを示すために手元のgccやperlを試してみたものの、再現しなかった。恐らくMSYS2でインストールしたため、MSYS2のデフォルトシェル(普通はbash)が設定されたのだろう。残念ながら、Haskellの
callCommand
関数はダメだった。手を抜きたいとき以外は、代わりにcallProcess
関数を使うこと。typed-processパッケージのshell
関数も同様。 ↩