JavaとJacksonでJSON その②XSS対策

#はじめに
前回の続きとして、XSS対策について触れてみたい。XSSといっても色々あるが、今回JSONの文字列の中にJava Scriptのコードを埋め込まれる場合の問題点にフォーカスしたい。

#環境
- Java 1.8
- Tomcat 8.0.53
- Jackson 2.10.1

#問題となる状況
前回サーバ側は、URLでGETでアクセスするとJSONを返却するサーブレットを作成したが、今回そのままそれを利用する。そして返却するJSONデータの中にJavaScriptのコードを埋め込んでみる。具体的には以下のJSONを返却するようにした。

```
{"id":1,"name":"kimisyo","datas":["Programmer","Data Scientist<script>alert('hello!')</script>"]}
```

うまくサニタイジングされないと、```<script>alert('hello!')</script>```の部分のJavaScriptのコードが実行されてしまう。

まずは、一般的な方法として、AjaxによりサーバのURLにアクセスし、その結果をJSONとして読み込み、中身を表示するJavaScirptを実行してみる。ソース(jspファイル)は以下の通りだ。

```clientTest.jsp
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>jQuery Hello World</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
<script>

$.ajax({
	url:"http://localhost:8080/servletTest/helloworld", // 通信先のURL
	type:"GET",		// 使用するHTTPメソッド
	// dataType:"json", // 応答のデータの種類
	dataType:"text", // 応答のデータの種類(xml/html/script/json/jsonp/text)
	timespan:1000 		// 通信のタイムアウトの設定(ミリ秒)
	}).done(function(data,textStatus,jqXHR) {
		var data2 = JSON.parse(data);
		alert(data2.datas);
	// failは、通信に失敗した時に実行される
	}).fail(function(jqXHR, textStatus, errorThrown ) {
		alert("error");
	// alwaysは、成功/失敗に関わらず実行される
	}).always(function(){
		alert("complete");
});


</script>
</head>
<body>
	<div id="test"></div>
</body>
</html>
```

このjspのURLをブラウザよりたたくと、以下のようにJSONのデータの中身が表示される。JSONに埋め込んだJavaScriptがそのままデータとして読み込まれており、問題のない動作である。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329816/4e913c4a-653f-2d64-9301-950165bf23d7.png)


では、次にJSONを返却するサーブレットのURLを直接Edgeでたたいてみる。すると見事に埋め込んだJavaScriptがブラウザにより実行されてしまう。サーバ側の開発に携わる者としては見たくない動作だ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329816/895fc85c-fb80-05b5-90a5-e446370d5694.png)


#回避策
ブラウザが"<"と ">"で囲まれた箇所をhtmlのタグとして認識してしまったことが問題であるため、JSONの中の"<", ">" 等の特殊文字をUnicodeエスケープシーケンスに変換してやればよい。
まずは、以下のクラスを作成する。エスケープしたい文字を、CustomCharacterEscapesメソッドに追加していく。

```CustomCharacterEscapes.java

package servletTest;

import com.fasterxml.jackson.core.SerializableString;
import com.fasterxml.jackson.core.io.CharacterEscapes;

public class CustomCharacterEscapes extends CharacterEscapes {

	private final int[] asciiEscapes;

    public CustomCharacterEscapes()
    {
        int[] esc = CharacterEscapes.standardAsciiEscapesForJSON();
        esc['"']  = CharacterEscapes.ESCAPE_STANDARD;
        esc['\''] = CharacterEscapes.ESCAPE_STANDARD;
        esc['/']  = CharacterEscapes.ESCAPE_STANDARD;
        esc['\n'] = CharacterEscapes.ESCAPE_STANDARD;
        esc['>'] = CharacterEscapes.ESCAPE_STANDARD;
        esc['<'] = CharacterEscapes.ESCAPE_STANDARD;
        asciiEscapes = esc;
    }

    @Override
    public int[] getEscapeCodesForAscii() {
        return asciiEscapes;
    }

    // no further escaping (beyond ASCII chars) needed:
    @Override
    public SerializableString getEscapeSequence(int ch) {
        return null;
    }
}
```

次に前回のサーブレットのところで一部修正する。

```ServletTest.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 com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;


// 参考　https://itsakura.com/java-jackson

@WebServlet("/helloworld")
public class ServletTest 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());

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

    		//JSONの出力
    		response.getWriter().write(testJson);
    	} catch (JsonProcessingException e) {
    		e.printStackTrace();
    	}
    }
}
```

具体的には、```mapper.getFactory().setCharacterEscapes(new CustomCharacterEscapes());```を追加している。

# さあ実験してみよう
上のServletのURLをブラウザより直接たたくとブラウザに以下の通り表示される。Unicodeシーケンスに変換されており、埋め込んだJavaScriptのコードは実行されない。

```
{"id":1,"name":"kimisyo","datas":["Programmer","Data Scientist\u003Cscript\u003Ealert(\u0027hello!\u0027)\u003C\u002Fscript\u003E"]}
```

では、Unicodeシーケンスに変換しても、JSONを受け取ったJavasScirpt側では正しくJSONデータとして解釈されるだろうか。
このJSONをJavaScriptでAjaxでアクセスして表示させると、変換しない場合と同様に、"<"や">"がJSONのデータとして読み込まれた結果が表示され、正しく解釈されたことが分かる。

#おわりに
次は HTMLにJSONを埋め込んでJavaScript から利用するケースの問題点について取り扱ってみたい。

#参考
- [[Java] Jackson でマルチバイト文字をエスケープする](https://atuweb.net/201608_java-jackson-escape-multibytechars/#%E3%83%9E%E3%83%AB%E3%83%81%E3%83%90%E3%82%A4%E3%83%88%E6%96%87%E5%AD%97%E3%82%92%E3%82%A8%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%97)