なぜクラスを作らなかったっ・・・!!?

この間の記事へのリアクションで、EncodedTextとかPersonクラスとかを作れよ、っていう意見がいくつかありました。

    ___,,,,... 、 -‐ァ´/'''7
   f r―-- ...,,,,__/./  /
.  | | |j~    //"'\/  
  | |    ヾソ /ミ三彡ゝ,
  | | |j~ /,.<\     //\ 
  | `‐-//_ゝ"    i!/__ヽ.   /
.  | r‐.//<´ ̄`ヾ u. /´ ̄フ } ./ /
.  | レ´,.イ | ヾヽ 。 /   `i.゚=彡,.レ´ /   なぜクラスを作らないっ・・・!!!?
,、-'´,、.'_`| |  ゞ三(  |j u ト.ァ''´,、-'´
 ,、i.| f. `| | /二ノ } u ,、-'´,、-'-、|
'´ | l に.|.| r‐t-,.、/,、-'´,、-'´u. ) u\   どうしてっ・・・・・・!? 
.  /| ゝァ|.| |二ン-'´,、,=;'_,=,=,´,-,‐n i
  |::::|. / `!,!-'´,、-,'〒〒〒〒〒〒.ヲ }__   カイジッ・・・・ !!
/::::::| !,-'´,、-'´ ̄ ̄ ̄ ̄ ̄ ̄ ̄  {::::
::::::、-'´,、-´|  u     三三三 u.  !:::   カイジィィィ・・・・・!!
-'´,、-.リ   '、__       u  /:::::
-'´:::::::|\ u      ̄ ̄"''‐-、._/|::::::::::
::::::::::::::|  \               /|::::::::::

なぜそうしないかというと、それをするとビューがEncodedTextやらPersonに依存してしまうからです。

なぜ依存しちゃダメなんでしょうか。その前にちょっと安全なHTMLテンプレートエンジンについて考察してみます。

安全に使えるHTMLテンプレートエンジンの要件

その1:全ての変数はデフォルトでHTMLエスケープされること

CSVに吐き出すなら「"」のエスケープをうっかり怠っても単に表が崩れるだけですみます。しかしHTMLで吐き出すなら、特にwwwに公開するサービスのHTMLを吐き出すならば、うっかりエスケープを怠るand/orエスケープの意義を知らない間抜けがチームに混じっていると破滅的な結末(XSS)を引き起こす場合があります。

(人生を豊かにするためのヒント:間抜けは存在する。)

そうならないために、デフォルトでは安全に処理が行われるようにする必要があります。

従って、HTMLを吐き出すテンプレートエンジンはデフォルトで全ての変数をHTMLエスケープするか、あるいはそれを強制するような使い方が可能でなければなりません。

なお、「簡単にHTMLエスケープできる」ではダメです。

例えば以下の擬似コードのように、ビュー側でいちいちエスケープ処理を行わなければならないテンプレートシステムではいつか誰かが必ずエスケープを忘れます。僕だっていつか忘れます。そのような設計は、人間の本質的な弱さを考慮していません。

ダメな設計の例
コントローラ

# $messageの値は本当はモデルから持ってきたよ!
my $stash = {
  message => "<script>alert('こんにちは!こんにちは!')</script>"
};
my $template = new BadHtmlTemplate();
$template->proccess($path, $stash);

ビュー

html_escapeすれば簡単にHTMLエスケープできるよ!
<div>
[% message.html_escape() %]
</div>

でもたまには忘れちゃう人もいるよね・・・
<div>
[% message %]
</div>

実行結果

<!-- html_escapeすれば簡単にHTMLエスケープできるよ! -->
<div>
&lt;script&gt;alert(&#39;こんにちは!こんにちは!&#39;)&lt;/script&gt;
</div>

でもたまには忘れちゃう人もいるよね・・・
<div>
<script>alert('こんにちは!こんにちは!')</script>
</div>

あくまでデフォルトでHTMLエスケープされなければなりません。
よい設計の例
コントローラ

# $messageの値は本当はモデルから持ってきたよ!
my $stash = {
  message => "<script>alert('こんにちは!こんにちは!')</script>"
};
my $template = new BetterHtmlTemplate();
$template->proccess($path, $stash);

ビュー

デフォルトでエスケープされるよ!
<div>
[% message %]
</div>

実行結果

デフォルトでエスケープされるよ!
<div>
&lt;script&gt;alert(&#39;こんにちは!こんにちは!&#39;)&lt;/script&gt;
</div>

その2:HTMLエスケープをすべきでない特別な場合にも対応できること

盲目的に全変数をHTMLエスケープしてしまうと満足できない要件が存在します。

  • URLっぽい文字列を自動的にリンクにしたい
  • 改行が改行に見えるように、行ごとに<p></p>で囲うか、改行を<br />に置き換えたい
  • WiKi記法のようなあらかじめ決められた規則に従って安全にHTMLを生成したい

のような場合です。どれも切り捨ててしまうにはもったいない、ごく自然な要求です。

さて、どうやってこの二律背反を解消しましょうか。

世の中色々なテンプレートエンジンが存在しますが、上で述べたような人間の弱さに優しいHTMLテンプレートエンジンというのは少数なように思います。Javaの世界でいうと、JSPもVelocityもFreeMarkerもビュー側でエスケープ処理を呼ばなければならないものに見えました。

じゃあテンプレートエンジンを自作しましょうか?魅力的な選択肢ですがそれなりに手間がかかりますよね。

現実的な選択肢の一つはこうです:

  • テンプレートエンジンに渡す直前にすべての変数をHTMLエスケープする
  • エスケープされるべきでないHTMLを含むような特別なデータは、例えば"unsafe"のようなオブジェクトに突っ込んで、"unsafe"にデータを入れている部分とビュー側で"unsafe"な値を使用している部分を注意深くテストする。
  • 特に間抜けと目されている人物が"unsafe"を使うのを可能な限り抑止する

コントローラ

my $safe = {
  message1=>"<script>alert(document.cookie)</script>"
};
my $unsafe = {
  message2=>linknize("http://www.yahoo.co.jp <script>alert(document.cookie)</script>")
};
# 第三引数の$unsafeに渡されているデータは慎重に検査せよ!
my $html = practical_template($path, $safe, $unsafe);

# linknize は、URLぽい文字列を 
# '<a href="http://...">http://...</a>' 
# のように置き換え、
# それ以外の部分をHTMLエスケープする、慎重に単体テストされた関数。
# 例えば linknizeに
# "http://www.yahoo.co.jp <script>alert(document.cookie)</script>"
# を渡すと、
# '<a href="http://www.yahoo.co.jp">http://www.yahoo.co.jp</a> &lt;script&gt;alert(document.cookie)&lt;/script&gt;'
# のような値を返す。

sub practical_template {
  my ($path, $safe, $unsafe) = @_;
  my $stash = {
    safe => html_escape_recursive($safe),
    unsafe => $unsafe
  };
  # SomeTemplate は HTMLを意識しない、
  # ごく普通のテンプレートエンジン
  my $template = new SomeTemplate();
  $template->process($path, $stash);
}

ビュー

safe だけを使ってる分には安全
<div>[% safe.message1 %]</div>

unsafe が使われていたら注意せよ!
<div>[% unsafe.message2 %]</div>

実行結果

safe だけを使ってる分には安全
<div>&lt;script&gt;alert(document.cookie)&lt;/script&gt;</div>

unsafe が使われていたら注意せよ!
<div><a href="http://www.yahoo.co.jp">http://www.yahoo.co.jp</a>
&lt;script&gt;alert(document.cookie)&lt;/script&gt;</div>

本題に戻る

さて、本題に戻ると、EncodedTextやPersonクラスを作った場合、こいつらがHTMLを知っているかどうか(=値が既にHTMLエスケープされているかどうか)どうやって知りましょうか。

普通に考えると、特定のプレゼンテーション(例えばHTML)を意識したデータコンテナを作る意味はないでしょうから、HTMLを知らないと仮定して一律エスケープしちゃったほうがよさそうです(実際それが安全サイドです)。

ではEncodedTextやPersonオブジェクトがもつデータをどうやってHTMLエスケープしましょうか。

エスケープ処理はビューでする
→人に厳しいですね。あとビュー側でEncodedTextやらPersonやらその他1000種類ある小さなデータコンテナから値を取り出す方法を知らなきゃダメですか?
エスケープ処理はEncodedTextやPersonやその他1000種類ある小さなデータコンテナを全て知っている
→ゲー
リフレクションを使う
それしかない?

それよりは、ビューはたった一つのvarクラスを知っていて(ということはビューを書くデザイナさんはvar文字列、varリスト、varハッシュから値を取り出す標準的な方法だけを覚えればよい)、あとはsafeとunsafeのどの位置に求めるデータが存在するかをコントローラ作者と申し合わせておけばよいんではないんでしょうかね。