JavaとJacksonでJSON その③HTMLにJSONを埋め込んでJavaScript から利用する

#はじめに
JSONを使う場合、JavaScriptからAjax経由でデータの送受信を行うケースが多いと思う。しかしながら、サーバからHTMLを受信したタイミングで、JSONデータを受け取ってJavaScriptで利用したいケースもある。この場合、サーバから返却するHTMLの中にJSONデータを埋め込んで、それをJavaScriptのオブジェクトとして読み込むことになる。PHPを利用した場合は、[HTML に JSON データを埋め込んで JavaScript から利用する](https://qiita.com/hoto17296/items/197bdf91f97a33a69dfc)に記載の事例があったが、我らがJava(Servlet/JSP)による事例がなかったため、悪戦苦闘した結果をここに残しておく。
#環境
- Java 1.8
- Tomcat 8.0.53
- Jackson 2.10.1

#まずは何も考えずにやってみよう⇒失敗
## サーバ側
サーバ側は以下の通りとした。"<"や">"については、[前回](https://qiita.com/kimisyo/items/694223f968094aad270c)同様、HTMLのタグとして解釈される恐れがあることから、Unicodeエスケープシーケンス変換はそのままとしている。前回までとの違いは、JSON文字列を、HttpRequestのパラメータとして保存し、それをJSPに処理させている点だ。詳しくはクライアント側の方で解説する。
また、意地悪データとして、```"Programmer"⇒"Programmer\"```のようにデータの最後に\を入れてみた。

```ServletTest2.java
package servletTest;

import java.io.IOException;
import java.util.List;
import java.util.ArrayList;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletContext;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@WebServlet("/helloworld2")
public class ServletTest2 extends HttpServlet {
    private static final long serialVersionUID = 1L;
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

    	//Javaオブジェクトに値をセット
    	JsonBean jsonBean = new JsonBean();
    	jsonBean.setId(1);
    	jsonBean.setName("kimisyo");
    	List<String> datas = new ArrayList<>();
    	datas.add("Programmer\\");
    	datas.add("Data Scientist<script>alert('hello!')</script>");
    	jsonBean.setDatas(datas);

    	ObjectMapper mapper = new ObjectMapper();
    	mapper.getFactory().setCharacterEscapes(new CustomCharacterEscapes());

    	//JavaオブジェクトからJSONに変換
    	String testJson = mapper.writeValueAsString(jsonBean);

    	//JSON文字列をrequestにセット
    	request.setAttribute("jsonStr", testJson);
    	ServletContext sc = getServletContext();
    	sc.getRequestDispatcher("/clientTest2.jsp").forward(request, response);

    }
}
```

##クライアント側
クライアント側はjspに処理を記載している。HttpRequestのパラメータとして設定されたJSON文字列を一旦Javaの変数に保存し、それをJavaScriptの中のJSONのParseの引数に設定している。
これがうまくいけば、dataというオブジェクトを通して、JSONのハンドリングが可能となる。

```clientTest2.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%
	String jsonStr = (String)request.getAttribute("jsonStr");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<script>
	var data = JSON.parse('<%=jsonStr%>');
　　　　 alert(data.datas);
</script>
</head>
<body>
</body>
</html>
```

## 失敗状況
上のServletを実行してみると、JSON.parseのところで```SCRIPT1014: 文字が正しくありません。```というJavaScriptのエラーがでる(IEのコンソールで確認)。最終的にブラウザに出力されたHTMLは以下の通りだ。
JSONに入れた\は、Jacksonによって\\\にちゃんとエスケープされているし、"<"や">"はUnicodeエスケープシーケンスに変換されている。一見問題なさそうに見える。

さぁ、何がまずかったのか考えて見よう。

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<script>
	var data = JSON.parse('{"id":1,"name":"kimisyo","datas":["Programmer\\","Data Scientist\u003Cscript\u003Ealert(\u0027hello!\u0027)\u003C\u002Fscript\u003E"]}');
	alert(data.datas);
</script>
</head>
<body>
	<div id="test"></div>
</body>
</html>
```

#失敗原因
失敗原因はJavaScript文字列のエスケープ漏れだ。JavaScriptでは、\はエスケープ用の文字として使われる。このため、```"Programmer\\"```はJavaScriptの文字列として```"Programmer\"```と認識される。これがJSON.Parseに引き渡されるが、JSONでも本来"\"の文字自体は、"\\\"のようにエスケープしなければならないため、不正なJSONデータとして扱われるのだ。

#対策
対策としては、JavaScript用のエスケープ処理をかました上でJSON.parseの引数に与えればよい。修正ソースを以下に記載しておく。ここではJavaScript文字列のエスケープとして、"\"と"'"をエスケープするための処理を入れている。

```clientTest2.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%
	String jsonStr = (String)request.getAttribute("jsonStr");
	jsonStr = jsonStr.replace("\\", "\\\\");
	jsonStr = jsonStr.replace("'", "\\'");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<script>
	var data = JSON.parse('<%=jsonStr%>');
　　　　 alert(data.datas);
</script>
</head>
<body>
</body>
</html>
```

これにより、正しくJSON文字列がJSONオブジェクトに読み込まれた。
余談であるが、JavaScript用に"\"をエスケープした場合、Unicodeエスケープシーケンス変換した文字(例えば、"\u0027")に使われている"\"もエスケープされ"\\\u0027"に変換されておかしくならないかという思うかもしれない。
実は、"\\\u0027"はJavaScriptによって"\u0027"と解釈され、それがJSON.parseの引数に与えれるため、JSON側で、Unicodeエスケープシーケンスとして解釈されているのだ。つまり前回のXSS対策の場合と同じ形のものがJSONに読み込まれており、むしろこちらの方が意図した動作となっているのである。JavaScript用のエスケープをする前は、実はHTML側でUnicodeエスケープシーケンスとして解釈されていたのだ。うーん、奥が深い。

#おわりに
結局JavaScriptエスケープだったという地味なオチ。この連載を始めたときに、この記事を書きたいと思っていたので、とりあえず完結としたい。

ちなみに本記事のサーバ側の例で、WEBフレームワークを使わずにServletを使って説明している理由は、別にServletしか使えない、Servletを使いたいわけではなく、本記事のテーマに関係ない要素は除外し、本質的な部分のみにフォーカスしたかったためである。

#参考
- [HTML に JSON データを埋め込んで JavaScript から利用する](https://qiita.com/hoto17296/items/197bdf91f97a33a69dfc)
- [JavaScript文字列のエスケープ](https://blog.ohgaki.net/javascript-string-escape)