結局Monoと.NETの挙動の違いはなんだったのか

続・アレな文字をWebClient.DownloadString(String)に渡すとローカルのファイルが読める

ここ2つの記事でMonoのWebClient.DownloadString(string)にアレな文字列渡すとローカルファイルを落としてきてしまうという挙動について調べてたわけですが、よくよくスタックトレースを見てみると、.NET FrameworkでもGetUriというメソッドを経由してPath.GetFullPathが呼ばれていたことがわかりました。

そんなわけでPath.GetFullPathの挙動を見てみると、http://../../../../etc/passwdといったような文字列を与えたときに.NETとMonoで次のような挙動の違いが見られました。

  • .NET Frameworkは「URIフォーマットはサポートしていない」というメッセージと共にArgumentExceptionが投げられる
  • Monoはそのままフルパスに変換する

.NET上で同じことをしてもDownloadString(string)がここまで問題にしてきた挙動をしなかったのは、ここで例外を吐いて止まっていたから、というだけのことだったわけです。

スキーム

ところでスキームをhttpじゃなくて適当な何かに変えたらどうなるんだろうと思って試してみました。

与える文字列をabc://../../../../etc/passwdといったように、実在しないような適当なスキームに変えて同じプログラムを動かしたところ、.NETではUriとしてのパースに成功しました。httpスキームのときは最初の部分をhostnameとして認識していたので、恐らくスキームを見てフォーマットを認識してるんでしょう。

さて、.NETにおいてはパースの結果abc://../etc/passwdになりました。なぜか../の部分がひとつにまとめられていましたがこれは一体どういう挙動なんでしょう。それはともかく、この結果を踏まえた上で同じ文字列をWebClient.DownloadString(string)に渡すと、今度は「そんなURIプリフィックスは知らん」とNotSupportedExceptionを投げられました。WebRequest.Createの中から呼ばれているようなので、前述のGetUriなどは成功しているようです(というか、new Uriが成功するんだからそら成功するだろう)。で、System.Net.WebRequest.Create(String)を見ると、やはり渡されたURIのスキームに対応するものがない、という例外でよさそうです。どんなURIが渡されてもこれが呼ばれるとするなら、http(s)://ftp://file://のいずれにも該当しないURIは常に弾かれることになります。

Monoの場合、まずnew Uri(String)UriFormatExceptionで失敗します。ということは前の記事で述べた通りPath.GetFullPathが呼ばれることになり、やはりこの場合でもローカルのファイルが読めてしまいます。

まとめ

結局何がこの挙動の違いを生み出していたのかというと以下の2点だと考えられます。

  • Monoのnew Uri(String)が未知のスキームを持つURIに対して失敗する(.NETは成功する)
  • MonoのPath.GetFullPath(String)がURIであるような文字列に対して成功する(.NETは失敗する)

とりあえずバグレポートも書いたのでこの件は一段落ということでいいんじゃないんでしょうか……(遠い目)

(バグレポート、type=“text”なフィールドでEnter叩いちゃって途中送信されて慌てて削除する方法を探してみるも見当も付かず、結局コメントで「途中送信しちゃった許してくださいお願いします何でもしますから」って言いながらレポート書き直したのは内緒だぞっ)

あと、整理のためにDownloadString(String)の処理の流れを書いておきます。

.NET

既知のスキームを持つ不正なURI

given: http://../../etc/passwd

  1. GetUriが呼ばれる
  2. たぶん内部的にnew Uri(String)して失敗する
  3. たぶんその結果Path.GetFullPath(String)が呼ばれる
  4. Path.GetFullPath(String)がArgumentExceptionを投げる (“URI formats are not supported.”)

未知のスキームを持つ不正なURI

given: abc://../../etc/passwd

  1. GetUriが呼ばれる
  2. たぶん内部的にnew Uri(String)して成功する
  3. 成功したのでその結果をそのまま返す
  4. WebRequest.Createが呼ばれる
  5. WebRequest.Createが未知のスキームに対応できずNotSupportedExceptionを投げる

Mono

既知のスキームを持つ不正なURI

given: http://../../etc/passwd

  1. CreateUriが呼ばれる
  2. new Uri(String)して失敗する
  3. Path.GetFullPath(String)が呼ばれる
  4. Path.GetFullPath(String)が成功し、結果を返す (e.g. /home/etc/passwd)
  5. その結果をnew Uri(String)に渡す (結果file:///home/etc/passwdなUriが返る)
  6. それを取得する (この場合は(たぶん)そのファイルがないので例外を吐く)

未知のスキームを持つ不正なURI

given: abc://../../etc/passwd

  1. CreateUriが呼ばれる
  2. new Uri(String)して失敗する
  3. Path.GetFullPath(String)が呼ばれる
  4. Path.GetFullPath(String)が成功し、結果を返す (e.g. /home/etc/passwd)
  5. その結果をnew Uri(String)に渡す (結果file:///home/etc/passwdなUriが返る)
  6. それを取得する (この場合は(たぶん)そのファイルがないので例外を吐く)

※.NETとの対比で両方書いたが、Monoの場合いずれも処理の流れは全く同じ