#はじめに
JSONを使う場合、JavaScriptからAjax経由でデータの送受信を行うケースが多いと思う。しかしながら、サーバからHTMLを受信したタイミングで、JSONデータを受け取ってJavaScriptで利用したいケースもある。この場合、サーバから返却するHTMLの中にJSONデータを埋め込んで、それをJavaScriptのオブジェクトとして読み込むことになる。PHPを利用した場合は、HTML に JSON データを埋め込んで JavaScript から利用するに記載の事例があったが、我らがJava(Servlet/JSP)による事例がなかったため、悪戦苦闘した結果をここに残しておく。
#環境
- Java 1.8
- Tomcat 8.0.53
- Jackson 2.10.1
#まずは何も考えずにやってみよう⇒失敗
サーバ側
サーバ側は以下の通りとした。"<"や">"については、前回同様、HTMLのタグとして解釈される恐れがあることから、Unicodeエスケープシーケンス変換はそのままとしている。前回までとの違いは、JSON文字列を、HttpRequestのパラメータとして保存し、それをJSPに処理させている点だ。詳しくはクライアント側の方で解説する。
また、意地悪データとして、"Programmer"⇒"Programmer\"
のようにデータの最後に\を入れてみた。
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のハンドリングが可能となる。
<%@ 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エスケープシーケンスに変換されている。一見問題なさそうに見える。
さぁ、何がまずかったのか考えて見よう。
<!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文字列のエスケープとして、""と"'"をエスケープするための処理を入れている。
<%@ 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を使いたいわけではなく、本記事のテーマに関係ない要素は除外し、本質的な部分のみにフォーカスしたかったためである。
#参考