Viewにおけるエスケープ処理 [Ruby on Rails]

こんにちは!もとひろです。
今回は、Railsのviewにおけるエスケープ処理について書きたいと思います。


エスケープ処理とは

「その言語やプログラムにおいて特殊な意味を持つ文字列や記号を、別の文字列に置き換えたり特殊な意味を無効化すること」です。
エスケープ処理は、XSS対策のため必要不可欠なものとなっています。

XSSとは

クロスサイトスクリプティング」の略で、Webページのセキュリティの脆弱性につけこむサイバー攻撃の一種です。
例としては、入力ボックス(コメント投稿等)にスクリプト付きのリンクを含む内容を入力して罠を仕掛け、利用者がリンクをクリックすると別のWebサイトに遷移して悪意ある内容を表示する(個人情報を入力させようとする等)ことなどが挙げられます。

Railsのviewにおけるエスケープ処理

Railsにおいては、XSSに対する対策がフレームワークとして組み込まれています。
RailsのviewではStringオブジェクトを描画しようとする際、自動的にHTMLタグに対してエスケープ処理が行われます。

"&" => "&", 
">" => ">",
"<" => "&lt;",
'"' => "&quot;", 
"'" => "&#39;"


つまり勝手にエスケープ処理してくれているわけです。
例えば、リンクを<a>タグでそのまま貼り付けようとしてもエスケープ処理されます。

# erb
<% link1 = "<a href='http://example.com'>Example</a>" %>
<%= link1 %>
↓↓
# 実際のHTML
&lt;a href=&#39;http://example.com&#39;&gt;Example&lt;/a&gt;


実際にRailsでアプリケーションを作る際はこのような書き方はしませんが、インスタンス変数(@~)でformから受け取った情報を表示することはあるかと思います。
その際に自動的にエスケープ処理してくれていたというわけです。

勝手にエスケープしてほしくないとき

方法としては2つあります。

① html_safeを使う

html_safeメソッドは、Stringオブジェクトを ActiveSupport::SafeBufferオブジェクトに変換します。
「これは安全です」という札をStringオブジェクトに貼った、というようなイメージです。
ここで重要なのは、html_safe(api.rubyonrails.org) の以下の部分です。

Marks a string as trusted safe. It will be inserted into HTML with no additional escaping performed. It is your responsibility to ensure that the string contains no malicious content. This method is equivalent to the raw helper in views. It is recommended that you use sanitize instead of this method. It should never be called on user input.

訳します。

文字列を信頼できる安全なものとしてマークします。追加のエスケープは実行されずに HTML に挿入されます。文字列に悪意のあるコンテンツが含まれていないことを確認するのはユーザーの責任です。このメソッドは、ビューの raw ヘルパーと同等です。この方法の代わりにsanitizeを使用することをお勧めします。ユーザー入力では決して呼び出さないでください。


けっこう怖いことが書かれていました。

大切なのは、

  • そのStirngオブジェクトが安全かどうかは開発者が確認しなければならないということ

  • 利用者が行うユーザー入力のデータには絶対にhtml_safeを付けてはいけない

ということです。
だから、「安全札」をStringオブジェクトに貼ったイメージ、なんですね。
中身がエスケープ処理されたわけではない、中身の安全性は自己責任でチェック、ということです。
ちなみに、rawメソッド、erbでの<%== ~ %>html_safeと同じ意味です。

html_safe?

Active Supportには「(html的に)安全な文字列」という概念があります。
上記に書いた「安全札」がそれにあたります。
Stringオブジェクトはデフォルトでは「安全ではない札」が付いている状態です。
html_safeメソッドによって「安全札」が付きます。
html_safe?メソッドは、「安全ではない札」ではfalse、「安全札」ではtrueを返します。
デフォルトでは「安全ではない札」が付いているのでfalseになり、エスケープ処理されるのです。
中身が安全かどうかは関係ないことにご注意ください。

# erb
<% link1 = "<a href='http://ex1.com'>Ex1</a>" %>
<%= link1.html_safe? %>
<%= link1 %>

<% link2 = "<a href='http://ex2.com'>Ex2</a>".html_safe %>
<%= link2.html_safe? %>
<%= link2 %>
↓↓
# 実際のHTML
false
&lt;a href=&#39;http://ex1.com&#39;&gt;Ex1&lt;/a&gt;

true
<a href='http://ex2.com'>Ex2</a>

link2は「安全札」を付けたためエスケープ処理されていないことが分かります。

② sanitizeを使う

sanitizeメソッドは、指定したタグ(tags)や属性(attributes)のみそのまま残り、指定していないものは全て削除されます。
オプションにtagsattributesを指定できます。オプションに指定しない場合は、こちらに定義されているものが残されます。
rails-html-sanitizer/lib/rails/html/sanitizer.rb at main · rails/rails-html-sanitizer

# erb
<% link = "<a href='http://example.com'>Example</a>" %>
<%= sanitize link %>
<%= sanitize link, tags: %w(a) %>
<%= sanitize link, attributes: %w(href) %>

<% alart = "<script>alert('Error!');</script>" %>
<%= sanitize alart %>
<%= sanitize alart, tags: %w(a) %>
<%= sanitize alart, tags: %w(script) %>
↓↓
# 実際のHTML
<a href="http://example.com">Example</a>
<a href="http://example.com">Example</a>
<a href="http://example.com">Example</a>

alert('Error!');
alert('Error!');
<script>alert('Error!');</script>

<%= sanitize alart %>はデフォルトの許可リストのなかに<script>タグはないため、削除されています。
<%= sanitize alart, tags: %w(a) %><a>タグのみ許可しているので、<script>タグは削除されています。

デフォルトでsanitizeを設定

config/application.rbに記載することで、sanitizeのデフォルトを設定できます。

config.action_view.sanitized_allowed_tags = ['strong', 'em', 'a']
config.action_view.sanitized_allowed_attributes = ['href', 'title']


まとめ

今回のまとめは以下になります。

  • Railsのviewでは自動的にHTMLタグに対してエスケープ処理が行われる

  • 勝手にエスケープしてほしくないときは、sanitizeで必要なものだけ許可するようにする

  • html_safeはユーザー入力に使っては絶対にダメ!中身が安全になるわけではない!


今回も勉強になりました!
間違い等ありましたら、ぜひコメントで教えていただけたら幸いです。

参考リンク


おまけの話

正直な話、ぱRails輪読会でエスケープ処理のところを読むまで、
「ん?勝手にエスケープ処理されてるの?」
html_safeって中身も全部安全にしてくれるんじゃないの?」
って思ってました…。
Railsのプラクティスもだいぶ進んできたというのに…。
書籍読んでも1回じゃよく分からなかったのは、そもそも根本が理解できていなかったからでした。
今回調べてみて、以前よりは結構理解できたかなと思ってはいます。
が、まだまだ実践で使ったりしているわけではないので、今後に活かしていけるようにしたいと思います!

終わります👋