Java와 Jackson에서 JSON 그 ② XSS 대책

13025 단어 JSONJackson자바

소개



전회의 계속으로서, XSS 대책에 대해 접해 보고 싶다. XSS라고 해도 여러가지 있지만, 이번 JSON의 문자열 속에 Java Script의 코드를 임베드되는 경우의 문제점에 포커스하고 싶다.

환경


  • Java 1.8
  • Tomcat 8.0.53
  • Jackson 2.10.1

  • 문제가 되는 상황



    지난번 서버측은 URL에서 GET으로 액세스하면 JSON을 반환하는 서블릿을 만들었지만 이번에 그대로 그것을 이용한다. 그리고 반환하는 JSON 데이터 안에 자바스크립트 코드를 내장해 본다. 구체적으로는 다음의 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가 그대로 데이터로 읽혀져 문제가 없는 동작이다.



    그럼, 다음에 JSON을 반환하는 서블릿의 URL을 직접 Edge로 두드려 본다. 그러면 훌륭하게 묻힌 JavaScript가 브라우저에 의해 실행되어 버린다. 서버측 개발에 종사하는 사람으로서는 보고 싶지 않은 동작이다.



    해결 방법



    브라우저가 "<"와 ">"로 둘러싸인 부분을 html 태그로 인식해 버린 것이 문제이기 때문에 JSON 안의 "<", ">"등의 특수 문자를 유니코드 이스케이프 시퀀스 로 변환하면 된다.
    우선, 이하의 클래스를 작성한다. 이스케이프하고 싶은 문자를, 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을 브라우저보다 직접 두드리면 브라우저에 아래와 같이 표시된다. 유니코드 시퀀스로 변환되었으며, 내장된 자바스크립트 코드는 실행되지 않는다.
    {"id":1,"name":"kimisyo","datas":["Programmer","Data Scientist\u003Cscript\u003Ealert(\u0027hello!\u0027)\u003C\u002Fscript\u003E"]}
    

    그렇다면 유니 코드 시퀀스로 변환해도 JSON을받은 JavasScirpt 측에서는 올바르게 JSON 데이터로 해석 될 것인가?
    이 JSON을 JavaScript로 Ajax로 액세스하여 표시시키면 변환하지 않는 경우와 마찬가지로 "<"나 ">"가 JSON의 데이터로 로드된 결과가 표시되어 올바르게 해석된 것을 알 수 있다.

    결론



    다음은 HTML에 JSON을 포함하고 JavaScript에서 이용하는 경우의 문제점에 대해 다루고 싶다.

    참고


  • [Java] Jackson에서 멀티 바이트 문자 이스케이프
  • 좋은 웹페이지 즐겨찾기