Secret Staircase

JSP でデフォルト HTML エスケープ

JSP で HTML エスケープするには <c:out><fn:escapeXml> を使うわけですが、いちいち書くの面倒だし付け忘れるもありがちです。付け忘れがないことを確認するにはコードを全部見たり、静的解析ツールを使ったり、サイトに対して XSS 検出ツールを使ったり、とても大変です。

そこで、デフォルトでエスケープする方法を考えてみます。具体的には次の機能を実現します。

  • デフォルトでエスケープする
  • <noescape> タグで囲った部分はエスケープしない
  • SafeHtml インターフェースを実装したクラスはエスケープしない

これらは ELResolver を使うと ある程度 実現できます。この後その方法と限界を見てゆきます。

ELResolver の実装

ELResolver では String のエスケープを行います。ソースコード全文は長いので こちら を参照してください。

下記コードにより ELResolver チェインにより解決した値が String の場合はエスケープします。SafeHtml の場合はエスケープしません。

Object value = context.getELResolver().getValue(context, base, property);
if (value instanceof String) {
  return escape((String) value);
} else if (value instanceof SafeHtml) {
  String html = ((SafeHtml) value).getSafeHtmlValue();
  context.setPropertyResolved(true);
  return html;
} else {
  return value;
}

また <noescape> タグをサポートするために request に NO_ESCAPE_KEY 属性があると処理をスキップします。

Boolean noescape = (Boolean) jspContext.findAttribute(NO_ESCAPE_KEY);
if ((noescape != null && noescape)) {
  return null;
}

noescape タグのソースコードは下記ようなものです。noescape.tag

&lt;%@ tag pageEncoding=&quot;utf-8&quot; trimDirectiveWhitespaces=&quot;true&quot; %&gt;
&lt;% request.setAttribute(EscapeHtmlElResolver.NO_ESCAPE_KEY, true); %&gt;
&lt;jsp:doBody/&gt;
&lt;% request.removeAttribute(EscapeHtmlElResolver.NO_ESCAPE_KEY); %&gt;

検証用モデル

続いて準備として検証に使うモデルを用意します。Wiki ページを表す WikiPage とそのページ本文である WikiText です。

public class WikiPage {
  public WikiPage(String title, WikiText wikiText) {
    this.title = title;
    this.wikiText = wikiText;
  }

  public String getTitle() { return title; }
  public WikiText getWikiText() { return wikiText; }

  @Override public String toString() {
    return "WikiPage(" + title + ", " + wikiText + ")";
  }

  private final String title;
  private final WikiText wikiText;
}

WikiPage は String 型の title プロパティと WikiText 型の wikiText プロパティを持ちます。ログ出力やデバッグ時に便利なように toString() も実装してあります。

public class WikiText implements SafeHtml {
  public WikiText(String value) {
    this.value = value;
  }

  public String getValue() { return value; }
  @Override public String getSafeHtmlValue() { eturn value; }

  private final String value;
}

WikiText は <script> など危険なタグを取り除いた HTML を持ち、このままレンダリングすることが想定されています。インターフェース SafeHtml とそのメソッド getSafeHtmlValue() を実装することで表現しています。

public interface SafeHtml {
  String getSafeHtmlValue();
}

動作検証

プロパティに HTML タグを含む次のモデルを使って動作を検証します。

WikiPage wikiPage = new WikiPage(
  "<strong>Tokyo</strong>",
  new WikiText("<strong>Tokyo</strong> is the capital of Japan."));

まず title の出力。

デフォルトエスケープできた! でもこれには抜け道があって、String 以外のクラスのレンダリングではエスケープされません。

太字になっている部分は <strong> によるものです。WikiPage#toString() がエスケープなしで出力されていますね。HTML が含まれるクラスの toString を意図的に利用することは少ないと思いますがうっかりやってしまうかもしれません。toString を使っていないことの調査は大変そうです。

noescape タグでのエスケープスキップはちゃんと動きます。

WikiText は SafeHtml インターフェースを実装しているのでそのままでエスケープを回避できます。

noescape タグはあれば便利だけど、JSP ではなく Java コードで実現できる SafeHtml を使った方が、ビューの役割を限定できて良いと思います。自動テストも書きやすいしね。

まとめ

JSP では ELResolver を使ってある程度デフォルト HTML エスケープを実現できます。タグやクラス定義により明示的にエスケープしないことも可能です。ただし String 以外がレンダリングされるときはエスケープされない大きな抜け道があります。

自分にとってこれはちょっと許容できないなぁ...

  • 検証に使ったソースコードは github にあります
  • こちらのサイトで動作を確認できます
  • ELResolver の実装についてはこの記事が大変参考になりました