(解決)Javaでサニタイズするときの注意—Apache Commons Lang 2.xと3.0の場合

Commons Lang 3.0.1で修正されています。

Java 5をサポートする「Apache Commons Lang 3.0」がリリース、というニュースがありました。正確には、Java 1.4以下をサポートしない「Apache Commons Lang 3.0」がリリース、だと思いますが、それはまあいいとして、これに関連する少し深刻な話を忘れないうちに書いておきましょう。

「Javaプログラマは文字列の操作方法を確認した方がいいかもしれない」のつづきです。

HTML文書を書く時には、「𠮷野家(>_<)」のような文字列は、「𠮷野家(&gt;_&lt;)」のように書かなければなりません。「<」や「>」、「&」などは特殊な文字なので、文書中にそのまま書くことはできないのです。

効率を気にしないなら、次のように書いて置換するのが簡単です。

str = "\uD842\uDFB7\u91CE\u5BB6(>_<)";
str = str.replace("&", "&amp;")
        .replace("<", "&lt;").replace(">", "&gt;");
System.out.println(str);
//𠮷野家(&gt;_&lt;)

しかし、このようなコードをいつも書くのは面倒ですし、順番を間違える危険もあります。実は「'」や「"」を置換したかったりもします。

そこで、HTML中に直接書けない文字を置換するためのライブラリを使います。Apache Commons Langはそのようなライブラリの一つで、拙著『Webアプリケーション構築入門』でも紹介しました。次のように使います。

まずは最近リリースされたCommons Lang 3.0の場合です。

String str = "文字列(>_<)";
str = org.apache.commons.lang3.StringEscapeUtils.escapeXml(str);
System.out.println(str);
//文字列(&gt;_&lt;)

3.0は出たばかりですし、Java 5以上が必須なので、まだ2.xの方がほとんどでしょう。2.xでは次のようになります。

String str = "文字列(>_<)";
str = org.apache.commons.lang.StringEscapeUtils.escapeXml(str);
System.out.println(str);
//&#25991;&#23383;&#21015;(&gt;_&lt;)

Commons Lang 2.xでは、ASCII以外の文字は「&#コードポイント(10進表記);」という形(数値文字参照)に置換されるので、人間にはわかりにくくなっていますが、ウェブブラウザでなら「文字列(>_<)」と表示されます。

2.xと3.0の間には、(1) パッケージ名(2.6はlang、3.0はlang3)(2) 数値文字参照の使用(2.6では使う、3.0では使わない)、に関して違いがあります。

ここまでは問題ありませんが、対象文字列がサロゲートペアを含むようになると、いやな感じになります。

まずは3.0の場合。

String str = "\uD842\uDFB7\u91CE\u5BB6(>_<)";
str = org.apache.commons.lang3.StringEscapeUtils.escapeXml(str);
System.out.println(str);
//𠮷?野家(&gt;_&lt;

変なところに「?」が入って、最後の「)」が無くなっていますが、これは間違いです。数値文字参照を使わない3.0では、「𠮷野家(&gt;_&lt;)」にならなければなりません。

バグレポートを送ったので、この問題は3.0.1で解決されるはずです。(追記:修正されました。)

サロゲートペアを使う可能性が少しでもある人は、Commons Lang 3.0はスルーしましょう。

パッチを見てもらえばわかりますが、「1文字ずつ処理する」部分にバグがあったのです(参考:Javaプログラマは文字列の操作方法を確認した方がいいかもしれない)。

とりあえずの結論:Javaの文字列処理は難しすぎる。

次にCommons Lang 2.xの場合です。

str = "\uD842\uDFB7\u91CE\u5BB6(>_<)";
str = org.apache.commons.lang.StringEscapeUtils.escapeXml(str);
System.out.println(str);
//&#55362;&#57271;&#37326;&#23478;(&gt;_&lt;)

「𠮷」が「&#55362;&#57271;」という2つの数値文字参照になっていますが、これは間違いです。「&#134071;」という1つの数値文字参照にならなければなりません(参考:Using character escapes in markup and CSS)。

この問題を解決するパッチを送りましたが、「3.0で修正されている」としてスルーされてしまいました。間違いが2.xで修正されるとしたら、動作をそろえるためにStringEscapeUtils.unescapeXml(str)にも修正が必要だと思い、そのためのパッチも送ったのですが、こちらは現時点では間違った動作はしていないので、やはりスルーされてしまいました。

「3.0で修正されている」とは言っても、先に述べたように、2.xと3.0ではサポートするJavaのバージョンが違いますし、パッケージ名も変わっているので、2.xでも直しておいた方がいいと思ったのですが、まあ、仕方ありません。

Commons Lang 2.xを使っている人は、「サロゲートペアはサポートしない」と仕様書に明記しておきましょう。

最終結論:Javaの文字列処理は難しすぎる。