Hatena::ブログ(Diary)

Alone Like a Rhinoceros Horn

2010-02-26

Railsノート - link_to <string> は安全か?

redirect_to に引き続き、こっちも気になったので調べてみた。link_to は渡された String をサニタイズするか? それとも、それはこちらでやるべきなのか?

リファレンスには相変わらず書いてないので、ソースを見る。ググったらわかるようなことだと思うのだが、ソースを読んだ方が調べる過程が面白くていいのだ。勉強にもなるし。

結論から言うと、link_to にユーザーからの入力値を含む String を渡すときはこちら側の責任できっちりサニタイズする必要がある。(参照している Rails のバージョンは 2.3.5)

# actionpack/lib/action_view/helpers/url_helper.rb

      def link_to(*args, &block)
        if block_given?
          options      = args.first || {}
          html_options = args.second
          concat(link_to(capture(&block), options, html_options).html_safe!)
        else
          name         = args.first
          options      = args.second || {}
          html_options = args.third

          url = url_for(options)

          if html_options
            html_options = html_options.stringify_keys
            href = html_options['href']
            convert_options_to_javascript!(html_options, url)
            tag_options = tag_options(html_options)
          else
            tag_options = nil
          end

          href_attr = "href=\"#{url}\"" unless href
          "<a #{href_attr}#{tag_options}>#{name || url}</a>".html_safe!
        end
      end

生成したリンクに対し html_safe! というメソッドを呼んでいて、これが危険なタグを除去するメソッドのように見えるのだが……

# activesupport/lib/active_support/core_ext/string/output_safety.rb

        def html_safe?
          defined?(@_rails_html_safe) && @_rails_html_safe
        end

        def html_safe!
          @_rails_html_safe = true
          self
        end

実は全然違うどころかむしろ逆で、これは String に対し「これは安全な HTML です!」という印を付けるものだ。

こんな風に使われている↓

# actionpack/lib/action_view/safe_buffer.rb

module ActionView #:nodoc:
  class SafeBuffer < String
    def <<(value)
      if value.html_safe?
        super(value)
      else
        super(ERB::Util.h(value))
      end
    end

    # snip

つまり、html_safe! が呼ばれた String に関しては、h によるエスケープさえも行われずそのまま出力用のバッファに追記される。link_to では、内部で呼び出している url_for が安全な文字列を返すと完全に信頼し切っているわけだ。

では、肝心の url_for が渡された String をサニタイズしてくれるのかというと……

# actionpack/lib/action_controller/base.rb

      def url_for(options = {})
        options ||= {}
        case options
          when String
            options
          when Hash
            @url.rewrite(rewrite_options(options))
          else
            polymorphic_url(options)
        end
      end

ぷ。url_for は String を与えられるとそれをそのまま返すのであった。なので、

link_to parameter_from_user

みたいなことをしでかすと終わりで、見事な XSS脆弱性が出来上がることになる。

link_to <string> が内部で sanitize を実行しないのは、「除去すべきタグとそうでないタグを決めるのはアプリケーション側の責任」であり、フレームワーク側では(アプリケーションが期待する)適切な sanitize ができないからだと推測するが、この辺、フレームワークがよきにはからってくれるだろうなんて高をくくっていると痛い目をみる好例なので注意したい。それこそ、フレームワークの実装に信頼をおかず、

  • このメソッドは引数を適切にエスケープしないかも知れない
  • このメソッドは引数を適切にサニタイズしないかも知れない

と疑ってかかる態度が必要だと思う。いわゆる、「かも知れない」運転というやつ。まあ、当たり前のことなのかも知れないけど。

まとめ
  • link_to にユーザーからの入力値を含む String 渡すときは sanitize すべし