Vim
Windows
Security
コマンドプロンプト

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

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)のエラーが。

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を起動してしまったのである。

原因

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が使い物にならない状態になってしまう :scream:

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

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


  1. vim以外のサンプルを示すために手元のgccやperlを試してみたものの、再現しなかった。恐らくMSYS2でインストールしたため、MSYS2のデフォルトシェル(普通はbash)が設定されたのだろう。残念ながら、HaskellのcallCommand関数はダメだった。手を抜きたいとき以外は、代わりにcallProcess関数を使うこと。typed-processパッケージのshell関数も同様。