ガラシのパルプンテ頼み

地方産限界エンジニアのグローバル独り言

結局textareaから入力された文字列ってどう改行したらいいの? | Rails入門

はじめに

form_withなどで生成したテキストエリアのフィールドから入力した文章をerbで表示したときに改行が反映されなくて困ったという場面には、多くのRails初学者が直面してきたのではないかと思います。 「Rails 改行」などのワードで検索を試みるとhtml_safe, raw, sanitize, simple_formatなど様々なヘルパーに関する記事がヒットして「結局どれが最適解なの、、」と思った方も少なくないのではないでしょうか。僕もその一人でした。

今回はこの戦争を終わらせるべく、それぞれのヘルパーに関する特徴、場面ごとの使い分けについてまとめていきたいと思います。 想定する読者、解決できる問題は以下の通りです。

  • テキストエリアから入力した文字列をviewで表示させると改行が反映されない
  • 一部の改行は反映されたけど入力した見た目通りに改行ができない
  • とりあえず検索結果に表示されたヘルパーで囲うことで改行はできたけどやり方として合っているのか自信がない

前提知識

特殊文字のエスケープ処理

そもそも何故テキストエリアから入力した文字列を表示すると改行などのスタイルがうまく当たらないのか、まずそれぞれのヘルパーの特徴ついてまとめて行く前に前提知識について整理してみたいと思います。

Railsはデフォルトで<%= %>で囲われた文字列に特殊文字が含まれている場合、それらを無害な文字列に変換する機能を持っています。

ここでいう特殊文字とは具体的には「<」「>」「&」「"」 のような文字列のことで、これらの文字列を「&lt;」「&gt;」「&amp;」「&quot;」 などの無害な文字列に変換するということをしてくれています。

例えば<p>や今回議題として取り上げている改行タグである<br> などのHTMLタグにもこれらの文字列が含まれていることがわかりますね。

この特殊文字を無害な文字列に変換すること工程をエスケープと呼びます。

<  -->  &lt;
>  -->  &gt;
&  -->  &amp;
"  -->  &quot;

<!-- 例えば改行タグは以下のように変換されて出力されている -->
<br> --> &lt;br&gt;

では何故、Railsはこのような文字列の変換を行なっているのか?についてですが、先ほどから幾度も無害な文字列に変換すると言っているように、上述の特殊文字はプログラムの世界ではそれぞれ意味のある挙動をする記号となっており、この対策が為されていないとテキストエリアから悪意のあるコードを流し込まれて、それが表示されるタイミングで実行されてしまうことにより、アプリケーションに意図しない悪影響を与える可能性があるためです。

XSS(クロスサイトスクリプティング)

例えばRailsにデフォルトでエスケープ処理が施されていなかった場合に、テキストエリアから以下のようなコードを入力されたとします。

<script>
  alert('hoge');
</script>

このコードは<script> タグで囲われた中にあるJavaScriptのコードを実行するものです。エスケープ処理が行われていない場合、DBに保存されたこの文字列がviewに表示されるタイミングで実際に実行されて、ブラウザには実際にJavaScriptのアラートが表示されることとなります。

今回のように単純に文字列を表示するだけのアラートならいざ知らず、これに外部サイトにリダイレクトさせるコードを仕込んだりすることも勿論可能です。このようにアプリケーションの入力フォームから悪意のあるコードを流し込み、実行させることで、不当に情報を抜き取ったりデータを破壊したりする攻撃手法のことをXSS(クロスサイトスクリプティング)と呼びます。

このようなセキュリティの脆弱性を塞ぐために、Railsではviewに文字列を出力する際にデフォルトで特殊文字をエスケープする機能が備わっています。

また、エスケープと似た用途で使われる言葉にサニタイズというものがありますが、それぞれ以下のような意味を指しています。

  • アプリケーションにとって有害な文字列を「無害化」する行為全般がサニタイズ
  • 特別な意味を有する文字列を別のものに変換する処理そのものがエスケープ

例えば空港の荷物検査を例に挙げると荷物検査そのものがサニタイズ 、荷物検査のうち問題のある荷物を取り除く工程がエスケープに当たります。

ヘルパーの具体的な使い分けについて

html_safe

さて、前置きが壮大となりましたがここからはこれらの前提を考慮した上で、意図したスタイルをview上で反映させるための具体的なヘルパーとそれぞれの特徴をまとめていきます。

まずはhtml_safeヘルパーについてですが、要点をまとめると以下のような特徴が挙げられます。

  • レシーバーとして与えられた文字列を信頼できる安全なもとしてマークし、エスケープ処理を解除する
  • 改行タグが有効となりview側で改行を含めた表示が可能となるが、改行タグ以外のタグについても否応なく有効化されてしまう
  • ユーザーからの入力など信頼性の低い文字列に対して呼び出した場合、XSS脆弱性の危険性が伴う

html_safeはレシーバーとして与えられた文字列を安全であるとマーキングすることで、含まれる特殊文字に対するエスケープ処理を否応なく解除させるヘルパーです。これにより勿論改行タグである<br> もview上で有効となり、意図した見た目での表示は可能となりますが、改行タグに関わらず全てのタグに対してエスケープが効かなくなってしまいます。

api.rubyonrails.org

sanitize

次にsanitize ヘルパーについて。このヘルパーは第一引数として与えた文字列に対して、第二引数でtags: 配列として与えたHTMLタグのみをホワイトリスト形式で許可し、許可したタグ以外を文字列から削除します。

尚tagsに何のタグも与えなかった場合はデフォルトの設定が適用され、scriptなどの危険性の高い一部のタグが削除されますが、殆どのタグがそのまま削除されずに残る形となります。

# 第二引数なし
sanitize("<h2>タイトル</h2>")
=> "<h2>タイトル</h2>"

sanitize("<script>alert('hoge')</script>")
=> "alert('hoge')"

# 第二引数あり
sanitize("<h2>タイトル</h2>", tags: %w(strong em a))
=> "タイトル"

尚sanitizeのデフォルト設定で許可されているタグの一覧は以下の通りです。

ActionView::Base.sanitized_allowed_tags 
=> #<Set: {
    "strong",
    "em",
    "b",
    "i",
    "p",
    "code",
    "pre",
    "tt",
    "samp",
    "kbd",
    "var",
    "sub",
    "sup",
    "dfn",
    "cite",
    "big",
    "small",
    "address",
    "hr",
    "br",
    "div",
    "span",
    "dd",
    "abbr",
    "acronym",
    "a",
    "img",
    "blockquote",
    "del",
    "ins"
}>

api.rubyonrails.org

simple_format

simple_formatは引数として与えた文字列を以下の条件で加工します。

  • 文字列を<p>で括る
  • 改行コード(/n)は <br> に変換
  • 連続した改行は、</p><p> を付与
  • sanitizeオプションをtrueにすることで文字列に対してsanitizeメソッドを実行することで、上述のデフォルトで許可されているタグ以外を文字列から除去

github.com

引数として与えたHTMLにおける危険な部分をよしなにエスケープしつつ、改行を含んだスタイルもいい感じに表示してくれます。しかし懸念点として以下の2点のような内容も挙げられます。

  • 3つ以上の連続した改行が表現できない
  • それぞれの段落区切りにpタグで囲われるため、pタグに付与されるCSSにより文末に意図しないmarginが付与されるなど、スタイリングの面で融通が効かないシーンもある

例えば以下のような文字列を与えられた際に「こんばんは」と「おやすみ」の間の3行の改行は1行分の改行として表示されてしまいます。

text = <<EOL
おはよう
こんにちは

こんばんは

おやすみなさい
EOL

simple_format(text)
=> 
<p>
    おはよう<br>
    こんにちは
</p>
<p>こんばんは/p>
<p>おやすみなさい</p>

api.rubyonrails.org

safe_join

とはいえ複数行の改行を要する場面というのも無きにしも非ず。

そこで、複数行の改行を含む文字列をセキュアに表示するコードを以下に示します。

html = "おはよう\nこんにちは\n\nこんばんは\n\n\nおやすみなさい"
safe_join(html.split("\n"), tag(:br))

=> おはよう<br>
こんにちは<br>
<br>
こんばんは<br>
<br>
<br>
おやすみなさい

safe_joinメソッドの処理の流れは以下の通りです。

  1. 引数で渡された配列の各要素に対してエスケープ処理を実行

     # splitにより返却される配列の中身
     html.split("\n")
     => ["おはよう", "こんにちは", "", "こんばんは", "", "", "おやすみなさい"]
    
  2. 第二引数で渡されたタグでエスケープ後の各要素をjoin

  3. 任意のタグでjoinされた全体の文字列を返却する

これにより渡ってきた文字列全文を検証した上で、フォーム側で入力した改行の数をそのまま考慮した表示をview上で行うことが可能となります。

api.rubyonrails.org

まとめ

エスケープ処理

ユーザーからの入力値をそのまま表示してしまうとXSSの危険性が伴うため、Railsではviewへの表示の際にデフォルトで文字列に対してエスケープ処理を行なっている。これによりHTMLタグに含まれる記号が無害化されて、副作用としてタグが機能しなくなるため改行等がスタイルとして反映できない。

html_safe

ユーザーから入力された信頼性の低い文字列に対して、スタイリングの用途でこのヘルパーをそのまま当てるのは危険といえます。

sanitize

表示するHTMLに含まれるタグの数が1つ2つ程度なら問題ありませんが、WYSIWYGエディタなどのスタイルの自由度が高いエディタをユーザーの入力フォームとして採用した場合にはメンテナンスが一気に辛くなります。

例えばユーザーから入力されうるスタイルで表示するために10以上のHTMLタグを配列として渡す必要が出てきたり、エディタ側の改修によってsanitizeに認可するタグも変更する必要があるなどメンテナンス性が非常に悪いです。

  • 表示するHTMLに一定の規則が見込める
  • フォーム側の改修頻度がさほど高くない

など入力フォーム側の仕様と相談する必要性はありそうです。

simple_format

前述の通り複数の改行を含んだ文字列の表示などやpタグに対して付与されるスタイルにより思い通りの見た目で表示できない場合もありますが、レイアウトの面は勿論のこと、sanitizeオプションをtrueにすることでセキュリティ面おいても諸々をいい感じに表示・エスケープしてくれるので、改行を反映させたいというケースにおいて大抵はこのヘルパーの利用で事足りるのではないかと思われます。

safe_join

  • 複数行の改行をview側で正確に表示したい
  • pタグに付与されるmarginが表示の要件に対して不都合

などsimple_formatより更に厳格に入力時の見た目を再現する必要がある場合には、この方法も手札として持っておくといいでしょう。

結論

結論として、テキストエリアから入力された文字列の表示時に、改行を反映させたいという目的に対するヘルパーの使用優先度としては以下のようになるかと思います。

simple_format ≥ safe_join > sanitize