はじめに
HTMLの中にあるscriptタグなどの除去(サニタイズ)を行いたかったため、Railsにおけるサニタイズの方法のベンチマークを行い、ベンチマークにおいて最良と判断したgemを使ってサニタイズの処理を実装しました。本記事ではベンチマーク結果とsanitizeというgemを用いたサニタイズの方法についてまとめています。
実現したかったこと
実現したかったことは、下記のHTMLに含まれるようなscriptタグやaタグのhref属性値にJavascriptが含まれる場合を想定し、scriptタグを全て除去、aタグがhref属性値がURLなどの安全でない値の場合に除去することでした。
<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を準備しました。
<h1>test</h1>
<h2>test2</h2>
<h3>test3<script>console.log('test');</script></h3>
下記がLoofahを用いた場合の方法になります。
> 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
という設定名で下記のように設定を作ってみました。
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は下記のようになりました。
<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である。