RailsのJavaScript文字列エスケープ

(Last Updated On: 2018年8月13日)

RailsはJavaScript文字列エスケープメソッドをサポートしています。JavaScript文字列エスケープのエントリで様々な議論があったようです。弊社ではRailsアプリのソースコードも検査しているので、参考としてRailsのソースコードを確認したことを追記をしたました。Rails開発者に直して頂きたいので独立したエントリにしました。

参考:


結論から言うとRailsのJavaScript文字列エスケープメソッドには問題があります。問題点の指摘の前にどのようなコードになっているかを見てみます。


module ActionView
  module Helpers
    module JavaScriptHelper
      JS_ESCAPE_MAP = {
        '\\'    => '\\\\',
        '</'    => '<\/',
        "\r\n"  => '\n',
        "\n"    => '\n',
        "\r"    => '\n',
        '"'     => '\\"',
        "'"     => "\\'"
      }

      JS_ESCAPE_MAP["\342\200\250".force_encoding(Encoding::UTF_8).encode!] = '&#x2028;'
      JS_ESCAPE_MAP["\342\200\251".force_encoding(Encoding::UTF_8).encode!] = '&#x2029;'

      # Escapes carriage returns and single and double quotes for JavaScript segments.
      #
      # Also available through the alias j(). This is particularly helpful in JavaScript
      # responses, like:
      #
      #   $('some_element').replaceWith('<%=j render 'some/element_template' %>');
      def escape_javascript(javascript)
        if javascript
          result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] }
          javascript.html_safe? ? result.html_safe : result
        else
          ''
        end
      end

      alias_method :j, :escape_javascript

参考:GitHubのソース

処理している事は難しい事ではありませんが、私のブログを読んでいる方はRubyに慣れてない方も多いと思うので簡単に解説します。


      def escape_javascript(javascript)
        if javascript
          result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] }
          javascript.html_safe? ? result.html_safe : result
        else
          ''
        end
      end

がJavaScript文字列をエスケープするメソッドで


          result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] }

正規表現にマッチした文字をJS_ESCAPE_MAPで定義したマップに変換しています。つまり、以下のマップ定義に従って文字が置換されます。


      JS_ESCAPE_MAP = {
        '\\'    => '\\\\',
        '</'    => '<\/',
        "\r\n"  => '\n',
        "\n"    => '\n',
        "\r"    => '\n',
        '"'     => '\\"',
        "'"     => "\\'"
      }

      JS_ESCAPE_MAP["\342\200\250".force_encoding(Encoding::UTF_8).encode!] = '&#x2028;'
      JS_ESCAPE_MAP["\342\200\251".force_encoding(Encoding::UTF_8).encode!] = '&#x2029;'

ECMAScriptの仕様書にある\エスケープ(JavaScript仕様通りのエスケープ)によって特殊文字をエスケープし、Unicodeのページセパレータ文字の2文字をHTML文字参照に変換しています。(&#x2028; と &#x2029;)

このJavaScript文字列エスケープメソッドには問題が幾つかあります。

  • Unicodeページセパレータ文字を ‘&#x2028;’ ‘&#x2029;’ に変換しているが、これはHTMLのエスケープ仕様
  • '</' => '<\/',変換で<script>タグから抜け出る事を防止
  • HTMLタグが文字列中に現れる事に未対応
  • ” や ‘ が誤ってHTML属性としてマッチしてしまう事に未対応
  • 古いブラウザに存在したサニタイズを利用する攻撃に未対応
  • HEXエスケープでなく、\エスケープを利用している

順番に解説します。

現在のJavaScriptが対応しているECMAScriptは'</' => '<\/',のエスケープが必要ない文字列に対するエスケープ処理は動作します。

V8のd8コマンドの実行例


[yohgaki@dev ~]$ d8
V8 version 3.14.5.10 [console: readline]
d8> write("<\/\n");
</
d8> 

JavaScriptの歴史はECMAScriptの仕様から始まったのではなくNetScapeのLiveScriptとして90年代なかばに開発された歴史があります。恐らくこのエスケープ動作は歴史的な理由から現在もサポートされていると思われます。恐らくECMAScript 5.1をサポートする実装はこのエスケープをサポートしているので問題ないと考えられます。

しかし、JavaScriptは \ エスケープのみでなくHEXとUnicodeエスケープもサポートしています。HEXまたはUnicodeエスケープを行えばHTMLパーサーで誤って解釈される可能性がある文字列を確実にエスケープできます。

次にHTML文字参照に変換している点です。HTML中に記載されたJavaScriptのコードはまずHTMLパーサーによってパースされ、パースされたコンテンツはHTMLエスケープをデコード後に次の処理系に渡されます。つまり、JavaScriptに渡された時にはHTML文字参照はUnicodeに変換されています。JavaScript文字列エスケープ関数でHTML文字参照に変換する意味がありません。

また、JavaScript文字列の中にHTML文字参照があってもJavaScriptはデコードしません。つまり、Railsのjavascriptメソッドでエスケープしてしまうと、JavaScriptパーサーだけでパースされる場合には不正な文字列を生成する可能性があります。Unicodeをエスケープしたい場合はJavaScriptのUnicodeエスケープ記法(\uXXXX)を利用すべきです。

'</' => '<\/',変換する事で<script>タグから抜け出れないようにしている点です。ブラウザやHTMLコードにバグ(次の” ’の取り扱い参照)などが無ければ、非標準の'/' => '</',がデコードされる仕様によって一応の安全性は保たれています。しかし、堅牢なコードと評価する事は難しいです。

このエスケープメソッドではHTMLタグが出現する事を防止していません。HTMLタグがHTMLパーサーによって解釈されないようにする為、HTMLコンテンツ中ではタグとなる文字列は現れるべきではありません。これを防ぐ為には少なくとも < と > はHEXエスケープします。

” や ‘ が誤ってHTML属性値としてマッチしてしまう事に未対応である点です。こんな事をやる方がそもそも悪い!という意見があると思いますが、HTMLタグの属性値をクオートで囲まないユーザも居ます。分ってはいてもクオートを忘れてしまった場合やHTMLパーサーのバグの場合もあるでしょう。その様な場合、” や ’ がHTMLタグの属性値としてマッチしてしまう可能性があります。マッチしてしまった場合、< > をエスケープしていないRailsのメソッドでは、JavaScriptインジェクションが可能になります。

古いブラウザには特殊文字やアスキーコードの上位バイトをサニタイズ(オクテットのMSBが1の場合に削除)する仕様がありました。現在のブラウザではこのような仕様は無いとは思いますが、ブラウザの実装はWeb開発者には制御できません。新しいブラウザでこのような実装をしてしまう物が出てこないとも限りません。リスク低減の為に特殊文字など、誤解釈される可能性のある文字は全てHEXエスケープする方がより安全です。

最後にHEXエスケープでなく\エスケープを利用している点です。ここまでで\エスケープでなくHEXエスケープを用いた方が良いことは何度か解説したので既に理解されてると思います。HEXエスケープを用いればJavaScriptの特殊文字(n r v bなど)だけでなく、HTMLの特殊文字(< > ‘ ” &など) がHTMLパーサーに誤解釈されないようにエスケープできます。JavaScript仕様では今のところサポートされているエスケープ処理でないエスケープを行う必要もありません。

まとめると

  • 取り敢えずは危険ではないのでjavascriptメソッドは使っても大丈夫
  • 大丈夫であってもjavascriptメソッドは堅牢でない
  • javascriptメソッドはJavaScriptだけのコード用に使うと不正な文字列を生成する場合がある(あまり無いですが)
  • JavaScript文字列のエスケープの様なエスケープの方が堅牢

と言うことになります。ブラウザに対して間違いないHTMLを出力してる場合には、大問題だと慌てる必要はありません。しかし、ブラウザのバグの場合、JavaScriptインジェクションできたのはブラウザの責任と言えます。しかし、「セキュリティ専門家が推奨するエスケープ方法を用いなかったのでJavaScriptインジェクションできましたが、これはブラウザのバグです」ではクライアントやユーザーは納得しないと思います。これは直した方が良いと思います。

Rails開発に近い方、直して頂けると有難いです。

追記:
HTML文字参照にエスケープしてしまうバグは 出力先のシステムが同じでも、出力先が異なる、を意識する を実践していれば防げたバグだと思います。

浸透しているのでJavaScriptがサポートを無くす事はないと思います。ECMAScript 5.1の仕様では\の後にはどんな文字が来ても良い事になっています。

V8 implements ECMAScript as specified in ECMA-262, 5th edition, and runs on Windows (XP or newer), Mac OS X (10.5 or newer), and Linux systems that use IA-32, x64, or ARM processors.

https://code.google.com/p/v8/

ということなので、手元のd8ではこのエスケープ方法も動作します。しかし、この手法が良いのか?は別問題です。

参考:
JavaScript: / の \ によるエスケープのみによるセキュリティ対策は禁止

投稿者: yohgaki