僕が書いたNet::FTPのコードに脆弱性報告があり、修正版がリリースされた。関係者のみなさん、ありがとうございました。

問題があったのは以下のようなコードだった。

def getbinaryfile(remotefile, localfile = File.basename(remotefile), blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data f = nil result = nil if localfile if @resume rest_offset = File.size?(localfile) f = open(localfile, "a") else rest_offset = nil f = open(localfile, "w")

ここでKernel#openを使っていたのが問題で、 localfile が | で始まる文字列だと外部コマンドが実行されてしまう。 しかも、まずいことに localfile のデフォルト値は File.basename(remotefile) なので、悪意のあるFTP上に | で始まる名前のファイルがあって、ディレクトリ内のファイルをすべてダウンロードするようなプログラムを書いていると、クライアント側でコマンドが実行されてしまう。パイプ以外ではopen-uriをrequireしているようなケースでも何かまずいことができてしまうかもしれない。

普段書き捨てのコード以外ではFile.openの方を使うのだけど、確認したところ1997年8月13日リリースのruby-1.1a0のころからKernel#openを使っていたようだ。

ちなみにopen以外にも罠があって、例えば

p File.read("|echo hello") #=> "hello"

のようにしてもコマンドが実行されてしまう。

これは、Fileクラスには実はreadは定義されておらず、IO.readが呼び出されるため。 他には、binread, write, binwrite, foreach, readlinesなども同様の罠がある。 わざわざ File. と書いてパイプをオープンしている意図的なコードはそうそうないと思うので、Fileクラスにパイプをオープンしないバージョンを追加してオーバーライドした方がよいのではないかと思うが、今さら2.5には間に合わないだろう。

当面は引数をFile.expand_pathでフルパスに展開するとか、先にFile.statするなどすれば、上記のような罠を回避することができると思う。