XSS 공격 방어(HTML javascript 공격)

웹 개발에서 때때로 보안 위반에 대한 조치를 취하기 위해 여기저기서 연락을 받게 됩니다.

무엇보다도 XSS(Cross Site Scripting) 공격을 방어하도록 노력합시다.

1. 크로스 사이트 스크립팅(XSS)이란 무엇입니까?

이는 공격자가 세션을 가로채거나, 웹사이트를 수정하거나, 악성 콘텐츠를 주입하거나, 상대방의 브라우저에서 스크립트를 실행시켜 피싱 공격을 진행하는 것을 의미합니다.

간단합니다. 사용자가 자바 스크립트 코드를 저장하더라도 실행되지 않아야 합니다.그것은.

2. 공격방식(탐지방식)

일반적으로 취약점 탐지는 위의 코드를 입력할 수 있는 곳에 아래의 코드를 입력하고 저장한 후 코드가 동작하는지 확인하는 방식으로 이루어진다.

<script>alert("xss");</script>

<img src=xyz onClick="alert('xss');"/>

<img src=xyz onLoad="location.href="https://google.com"" />

<img src=xyz onMouseOver="window.open('https://googlg.com');" />

3. 공격 방어

방어는 위의 스크립트가 작동하지 않도록 하는 것입니다.

데이터 저장 시 문자열을 교체하여 저장이 경우 필터가 사용됩니다.

& &
> <
< >
( (
) )

Lucy-xss-filer를 사용했지만 사용자 정의가 어려워 필터를 추가하는 방향으로 개발되었습니다.

아래 단계에 따라 필터를 쉽게 적용할 수 있습니다.

먼저 두 개의 클래스가 필요합니다.

(1) 필터를 구현하는 클래스

-> 필터 체인을 적용하는 클래스

(2) HttpServletRequestWrapper를 상속받은 클래스

-> 요청에서 매개변수 또는 원시 데이터를 가져올 때 실제로 xss를 적용하는 클래스

필터링은 쉬우므로 먼저 수행해 보겠습니다.

//필터 이름은 알아서 만들자
public class xssCustomFilter implements Filter{
	@Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    
    	//((HttpServletRequest) req).getRequestURI(); 
        //이 코드로 URL를 분석하여 특정 URL에서는 filter가 동작하지 않도록 할 수 있다.        
        
    	HttpServletRequestBodyWrapper wrapper = new HttpServletRequestBodyWrapper((HttpServletRequest)req);
        chain.doFilter(wrapper, res);
    }
	
    @Override
    public void init(FilterConfig filterConfig) {}

    @Override
    public void destroy() {}

}

다음으로 BodyWrapper를 만듭니다.

GET 및 POST 호출을 모두 처리해야 하므로 약간 더 많은 코드를 추가합니다(주석 참조).

public class HttpServletRequestBodyWrapper extends HttpServletRequestWrapper {

    private String body;
    private byte() rawData;
    private String path = "";

    public HttpServletRequestBodyWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.path = request.getRequestURI();
        String _method = request.getMethod();
        String _contentType = request.getContentType();
        
        //method가 "POST"인데 ContentType이 null인경우가 있어서 처리해줫다.
        if(_method == null) _method = "";
        if(_contentType == null) _contentType = "";

        //ContentType이 contains인이유는 ContentType이 "application/json; charset=utf8"인 경우가 존재했기때문이다.
        if(_method.equalsIgnoreCase("post") && _contentType.contains("application/json")) {
            InputStream is = request.getInputStream();
            this.rawData = replaceXSS(IOUtils.toByteArray(is));
        }
    }

	//request에서 get방식일때 parameter값을 읽게 되고 
    //그때 xss 치환해주기위해 해당 코드를 넣어줬다.
    @Override
    public String getParameter(String name) {
        return replaceXSS(super.getParameter(name));
    }
    
    //get 방식 처리
    @Override
    public Map<String, String()> getParameterMap() {
        Map<String, String()> params = super.getParameterMap();
        if(params != null) {
            params.forEach((key, value) -> {
                for(int i=0; i<value.length; i++) {
                    value(i) = replaceXSS(value(i));
                }
            });
        }
        return params;
    }


	//get 방식 처리
    @Override
    public String() getParameterValues(String name) {
        String() params = super.getParameterValues(name);
        if(params != null) {
            for(int i=0; i<params.length; i++) {
                params(i) = replaceXSS(params(i));
            }
        }
        return params;
    }

    //POST방식의 처리
    // POST 방식의 호출은 requestBody 안에 데이터가 있기 때문에
    //생성자에서 rawData에 데이터가 들어있으므로 return해준다.
    //근데 해보면 new를 안하고 return할경우 '읽음'처리 되는순간 inputStream이 없어지므로 
    //copy하여 return해준다.  => 저도 잘 몰라요
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
    
    @Override
    public ServletInputStream getInputStream() throws IOException{
        if(this.rawData == null){
            return super.getInputStream();
        }
        final ByteArrayInputStream bis = new ByteArrayInputStream(this.rawData);

        return new ServletInputStreamImpl(bis);
    }

    private String getBody(HttpServletRequest request) throws IOException {
        StringBuilder sb = new StringBuilder();

        try (
                InputStream inputStream = request.getInputStream();
                BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))
        ) {
            char() charBuffer = new char(128);
            int bytesRead = -1;
            while ((bytesRead = br.read(charBuffer)) > 0) {
                sb.append(charBuffer, 0, bytesRead);
            }
        }
        return sb.toString();
    }

	//inputStream를 새로 생성하여 return해주기위한 내부 class
    class ServletInputStreamImpl extends ServletInputStream {
        private InputStream is;

        public ServletInputStreamImpl(InputStream bis) {
            is = bis;
        }

        public int read() throws IOException {
            return is.read();
        }

        public int read(byte() b) throws IOException {
            return is.read(b);
        }

        @Override
        public boolean isFinished() {
            return false;
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setReadListener(final javax.servlet.ReadListener readListener) {}
    }

    //Xss 공격방어
    private byte() replaceXSS(byte() data) {
        String strData = new String(data);
        strData = strData.replaceAll("<", "&lt;")
                        .replaceAll(">", "&gt;")
                        .replaceAll("(", "&#40;")
                        .replaceAll(")", "&#41;")
                        .replaceAll("(?i)javascript", "")
                    ;


        return strData.getBytes();
    }

    private String replaceXSS(String value) {
        if(value != null) {
            value = value.replaceAll("<", "&lt;")
                        .replaceAll(">", "&gt;")
                        .replaceAll("(", "&#40;")
                        .replaceAll(")", "&#41;")
                        .replaceAll("(?i)javascript", "")
                        ;
        }
        return value;
    }
}

마지막으로 web.xml에 필터를 추가합니다.

<filter>
    <filter-name>xssCustomFilter</filter-name>
    <filter-class>com.gas.secure.XssCustomFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>xssCustomFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

이 경우 GET 호출인지 POST 호출인지에 관계없이 XSSFiler가 적용됩니다.

4. 화면 표시

이제 “< >…”와 같은 데이터가 DB에 저장됩니다. 이제 이것을 화면에 표현해야 하는데 각 방법마다 표현 방법이 다릅니다.

화면에 데이터를 표시하는 방법에는 두 가지가 있습니다.

(1) 다음과 같이 태그 아래에 정보를 표시하는 방법 B. 화면의 div, label 및 span 태그

(2) input, textarea와 같은 태그에 값을 부여하여 화면에 표시하는 메소드

#1을 사용하면 DB 데이터를 화면에 직접 표시할 수 있습니다.

$('#div').append(result);

<div><c:out value="${result.data}" /></div>

“>”가 예상대로 화면에 나타납니다.

문제는 (2)

$('#input').val(result);
$('#textarea').val(result);

이 경우 입력 태그의 데이터도 <로 변경되므로 다음과 같이 표현한다.

이 경우 서버로부터 받은 결과 값을 자바스크립트에서 추가로 수정하여 입력 태그에 데이터를 삽입해야 한다.

var characterUnescapes = function(value) {
    var str = value.replace(/\&lt\;/g, '<')
    	.replace(/\&gt\;/g, '>')
        .replace(/\&quot\;/g, '"')
        .replace(/\&rsquo\;/g, "'")
        .replace(/\<script\>/ig, '&lt;script&gt;')
        .replace(/\<\/script\>/ig, '&lt;/script&gt;');
    return str;
};

$('#input').val(characterUnescapes(result));

종료에 대해