LoginSignup
5
3

More than 3 years have passed since last update.

JavaとJacksonでJSON その②XSS対策

Last updated at Posted at 2020-01-03

はじめに

前回の続きとして、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

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

image.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 から利用するケースの問題点について取り扱ってみたい。

参考

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3