LoginSignup
2
1

More than 1 year has passed since last update.

【Rails】gem sanitizeを用いてHTMLをサニタイズしてみた

Last updated at Posted at 2021-08-18

はじめに

HTMLの中にあるscriptタグなどの除去(サニタイズ)を行いたかったため、Railsにおけるサニタイズの方法のベンチマークを行い、ベンチマークにおいて最良と判断したgemを使ってサニタイズの処理を実装しました。本記事ではベンチマーク結果とsanitizeというgemを用いたサニタイズの方法についてまとめています。

実現したかったこと

実現したかったことは、下記のHTMLに含まれるようなscriptタグやaタグのhref属性値にJavascriptが含まれる場合を想定し、scriptタグを全て除去、aタグがhref属性値がURLなどの安全でない値の場合に除去することでした。

sample.html
<script>console.log('hoge');</script> 👈
<h1>header1</h1>
<h2>header2</h2>
<h3>header3<script>console.log('piyo');</script></h3> 👈

<a href='mailto:test@example.co.jp?subject=件名'>お問い合わせ</a>
<a href='https://www.test.com/?hl=ja'>ホーム画面</a> 
<a href='javascript:""'>aタグに埋め込まれたJavascript</a> 👈

<div>
  <div>
    <div>
      <script>console.log('fuga');</script> 👈
    </div>
  </div>
</div>

<table>
  <tr>
    <td>hoge<script>console.log('hogehoge');</script></td> 👈
    <td>fuga</td>
    <td>piyo</td>
    <script>console.log('fugafuga');</script> 👈
  </tr>
  <tr>
    <td>hogehoge</td>
    <td>fugafuga</td>
    <td>piyopiyo</td>
  </tr>
</table>

ベンチマークと検証

ActionView::Helpers::SanitizeHelper

まずは、Railsの機能のSanitizeHelperを用いることを試しました。
このhelperは、後述のloofahとrails-html-sanitizerというgemがベースになっているものになります。
カスタムして使いたい時は、:scrubberオプションを用います。

検証用に下記のHTMLを準備しました。

sample2.html
<h1>test</h1>
<h2>test2</h2>
<h3>test3<script>console.log('test');</script></h3>

下記がLoofahを用いた場合の方法になります。

Railsコンソール
> unsafe_html
=> "<h1>test</h1>\n<h2>test2</h2>\n<h3>test3<script>console.log('test');</script></h3>"

> html_fragment = Loofah.fragment(unsafe_html)
=>
#(DocumentFragment:0x35fc {
...

> scrubber = Rails::Html::TargetScrubber.new
=> #<Rails::Html::TargetScrubber:0x00007fb7911180a0 @attributes=nil, @direction=:bottom_up, @tags=nil>

> scrubber.tags = ['script'] # 👈 サニタイズするタグを指定
=> ["script"]

> html_fragment.scrub!(scrubber) # 👈 サニタイズ実行
=>
#(DocumentFragment:0x35fc {
  name = "#document-fragment",
  children = [
    #(Element:0x3610 { name = "h1", children = [ #(Text "test")] }),
    #(Text "\n"),
    #(Element:0x3624 { name = "h2", children = [ #(Text "test2")] }),
    #(Text "\n"),
    #(Element:0x3638 { name = "h3", children = [ #(Text "test3"), #(Text "console.log('test');")] })]
  })

> html_fragment.to_s
=> "<h1>test</h1>\n<h2>test2</h2>\n<h3>test3console.log('test');</h3>" # 👈 sciptタグを削除できた

sciptタグはサニタイズできましたが、scriptタグ内にあった文字列は残ってしまいました。
これについては、正規表現で文字列を置換することで対応しました。

unsafe_html.gsub(/<script>.*<\/script>/, "<script></script>")

一方、scrubber.tagsでタグの指定はできますが、より細かな設定ができないことが課題として残りました。

サニタイズできるgem

他にもサニタイズができるgemがないか探してみたところ、圧倒的スター数を持つsanitizeというgemを見つけました。

gem名 スター数 最終リリース日
sanitize 1.9K 2021.1.12
loofah 804 2021.4.7
rails-html-sanitizer 223 2019.10.7
htmlfilter 8 2012.12

sanitizeの開発者が「RubyのHTMLサニタイズライブラリーの偏った比較」というものをまとめていました。
Biased comparison of Ruby HTML sanitization libraries

It's heavily biased because I'm the author of Sanitize,

開発者は「私はSanitizeの開発者だから非常に偏った内容だよ」と言っていますが、内容を見るとスター数が物語っているように機能面とパフォーマンス面共に他者を圧倒していることが分かります。(内容は割愛します)

設定の自由度も高いため、sanitizeというgemを使う方向で検証を進めました。

sanitizeの使い方

基本的な使い方

基本的な使い方は、公式のReadmeと下記の記事を参考にさせていただきました。

railsでhtmlをsanitize gemでカスタマイズしてsanitizeしたい

設定のカスタム方法

デフォルトでサニタイズの強度別に4種類の設定があります。
各設定の設定項目は下記のファイルで参照できます。

https://github.com/rgrove/sanitize/blob/main/lib/sanitize/config/default.rb
https://github.com/rgrove/sanitize/blob/main/lib/sanitize/config/basic.rb
https://github.com/rgrove/sanitize/blob/main/lib/sanitize/config/relaxed.rb
https://github.com/rgrove/sanitize/blob/main/lib/sanitize/config/restricted.rb

カスタムで設定したい場合、initializerを作ってカスタムの設定を定義できます。
CUSTOMという設定名で下記のように設定を作ってみました。

config/initializers/sanitize.rb
class Sanitize
  module Config
    CUSTOM = freeze_config(
      :elements => %w[
        b em i strong u
                ~中略~
      ],

      :attributes => {
                           :all         => %w[class dir hidden id lang style tabindex title translate],
                           'abbr'       => %w[title],
                           'blockquote' => %w[cite]
                           # ~中略~
      },

      :protocols => {
                          'a'           => {'href' => ['http', 'https', 'mailto', :relative]}
                          # ~中略~
      },

      :remove_contents => %w[iframe math ~中略~ ], # デフォルト設定を明示的に記載

      :css => {

        :at_rules_with_properties => %w[
          bottom-center
          bottom-left
          ~中略~
        ]
      }
    )
  end
end

sanitizeを実際に使ってみた

実際にサニタイズを実行した結果が下記となります。

> unsafe_html = "<script>console.log('hoge');</script>
<h1>header1</h1></td>
<h2>header2</h2></td>
<h3>header3<script>console.log('piyo');</script></h3>

<a href='mailto:test@example.co.jp?subject=件名'>お問い合わせ</a>
<a href='https://www.google.com/?hl=ja'>Google</a>
<a href='javascript:""'>aタグに埋め込まれたJS</a>

<div>
  <div>
    <div>
      <script>console.log('fuga');</script>
    </div>
  </div>
</div>

<table>
  <tr>
    <td>hoge<script>console.log('hogehoge');</script></td>
    <td>fuga</td>
    <td>piyo</td>
    <script>console.log('fugafuga');</script>
  </tr>
  <tr>
    <td>hogehoge</td>
    <td>fugafuga</td>
    <td>piyopiyo</td>
  </tr>
</table>"

> html = unsafe_html.gsub(/<script>.*<\/script>/, "<script></script>")
=> "<script></script>\n<h1>header1</h1>\n<h2>header2</h2>\n<h3>header3<script></script></h3>\n\n<a href='mailto:test@example.co.jp?subject=件名'>お問い合わせ</a>\n<a href='https://www.google.com/?hl=ja'>Google</a>\n<a href='javascript:'>aタグに埋め込まれたJS</a>\n\n<div>\n  <div>\n    <div>\n      <script></script>\n    </div>\n  </div>\n</div>\n\n<table>\n  <tr>\n    <td>hoge<script></script></td>\n    <td>fuga</td>\n    <td>piyo</td>\n    <script></script>\n  </tr>\n  <tr>\n    <td>hogehoge</td>\n    <td>fugafuga</td>\n    <td>piyopiyo</td>\n  </tr>\n</table>"

> Sanitize.fragment(html, Sanitize::Config::CUSTOM) # 👈 sanitize.rbで定義した定数を使用
=> "\n<h1>header1</h1>\n<h2>header2</h2>\n<h3>header3</h3>\n\n<a href=\"mailto:test@example.co.jp?subject=件名\">お問い合わせ</a>\n<a href=\"https://www.google.com/?hl=ja\">Google</a>\n<a>aタグに埋め込まれたJS</a>\n\n<div>\n  <div>\n    <div>\n      \n    </div>\n  </div>\n</div>\n\n<table>\n  <tbody><tr>\n    <td>hoge</td>\n    <td>fuga</td>\n    <td>piyo</td>\n    \n  </tr>\n  <tr>\n    <td>hogehoge</td>\n    <td>fugafuga</td>\n    <td>piyopiyo</td>\n  </tr>\n</tbody></table>"

サニタイズ後のHTMLは下記のようになりました。

sanitized.html
<h1>header1</h1>
<h2>header2</h2>
<h3>header3</h3>

<a href=\"mailto:test@example.co.jp?subject=件名\">お問い合わせ</a>
<a href=\"https://www.google.com/?hl=ja\">Google</a>
<a>aタグに埋め込まれたJS</a>

<div>
  <div>
    <div>

    </div>
  </div>
</div>

<table>
 <tbody>
    <tr>
      <td>hoge</td>
      <td>fuga</td>
      <td>piyo</td>
    </tr>
    <tr>
      <td>hogehoge</td>
      <td>fugafuga</td>
      <td>piyopiyo</td>
    </tr>
  </tbody>
</table>

まとめ

  • sanitizeだけではscriptタグ内の文字列が除去されない。
  • gem sanitizeは細かな設定が可能で、サニタイズできるgemの中で優位性の高いgemである。
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1