> Hello World !!!

     

@syaku

Spring XSS Filter Annotation : Lucy Xss Filter - Secure Coding - 크로스 사이트 스크립팅

Spring XSS Filter Annotation : Lucy Xss Filter - Secure Coding - 크로스 사이트 스크립팅

진행하는 프로젝트에 xss 대한 공격을 방어하기 위해 xss filter 라이브러리를 도입하려고 한다. 방어 방법이 다양하지만 해킹이라는 것이 새로운 방식이 지속적으로 만들어져서 그에 대비한 확장성이 필요하다. 그래서 필터를 추가할 수 있는 화이트 필터를 찾고 있는 데 네이버에서 개발한 lucy xss filter 가 괜찮아 보여 도입하게 되었다.

[내용 추가] 위지윅에디터의 html 이나 스크립트가 입력되는 항목에만 필터를 사용해야한다. 일반적인 항목은 escape 를 이용하여 처리한다.

일반적으로 서블릿을 통한 모든 데이터를 필터하겠지만 난 이보다 선택적인 필터가 가능하도록 구현하려고 한다. 간혹 어쩔수 없이 요청된 데이터 그대로를 유지해야 하는 경우 개발자가 선택적이여야 하고, 요청된 데이터 타입(Content-Type)이 어떤 것인지도 판단되지 않는 데 우선적인 필터보다 요청된 데이터를 스프링을 통해 자동 바인딩된 객체 데이터를 필터하는 것이 더 효과적이라 생각했다. 그리고 선언적인 방식이 일반적인 프로그램에 의한 방식에 비해 코드 수가 줄어드니 어노테이션을 고려하게 되었다.

그래서 스프링 AOP 와 어노테이션을 잘 활용하면 내가 원하는 선언적인 방식을 구현할 수 있을 거라 판단하여 작업을 시작했다.

[경고] 자바 리플랙션에 대한 지식이 풍부하지 않은 상태에서 개발하였다. 꼭 소스를 직접 분석하고 잘못된 부분이 없는 지 판단하고 사용하시길 권장한다.

Github : https://github.com/syakuis/lucy-xss-annotation

테스트 소스가 포함되어 있어 실제 사용되는 소스파일은 아래와 같다.

/src/main/java/org/syaku/spring/boot/config/LucyXssFilterConfiguration.java
/src/main/java/org/syaku/spring/xss/support/**

라이브러리로 배포하기엔 너무 작은 프로젝트라 따로 묶지않고 소스 그대로를 배포하였다.

lucy xss 설치하기

https://github.com/naver/lucy-xss-filter

자세한 사용법은 위 링크를 통해 참고한다.

라이브러리 의존성 추가

<dependency>
    <groupId>com.navercorp.lucy</groupId>
    <artifactId>lucy-xss</artifactId>
    <version>1.6.3</version>
</dependency>

lucy xss 설정파일을 resources 최상위 경로에 넣는 다. 이미 저장소에 등록되어 있으니 git 을 복제했다면 하지 않아도 된다.

스프링에서 루시를 사용할 수 있게 스프링빈을 생성한다. 실제 소스가 포함되어 있어 그대로 사용해도 된다.

< / > LucyXssFilterConfiguration.java

import com.nhncorp.lucy.security.xss.XssFilter;
import com.nhncorp.lucy.security.xss.XssSaxFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.syaku.spring.xss.support.aop.XssFilterAspect;

@Configuration
public class LucyXssFilterConfiguration {

    @Bean
    public XssSaxFilter xssSaxFilter() {
        return XssSaxFilter.getInstance("lucy-xss-sax.xml", true);
    }

    @Bean
    public XssFilter xssFilter() {
        return XssFilter.getInstance("lucy-xss.xml", true);
    }

    @Bean
    public XssFilterAspect xssFilterAspect() {
        return new XssFilterAspect();
    }
}

선언적인 어노테이션 사용방법

@Defence 어노테이션은 @Controller 스테레오 타입에 한해 클래스 메서드 파라메터, 클래스(자식 포함), 클래스 항목에만 사용할 수 있다. 그리고 문자열만 필터된다.

public void method(
    @Defence String filter, // (1)
    @Defence Foo foo             // (2)
) { ...

메서드 파라메터가 (1)참조타입(기본타입)인 경우에만 필터가 작동하고 (2)클래스타입? 인 경우 클래스 내부에 또다시 @Defence 을 선언해야 한다. 다시말해 (2)번의 경우 필터 되는 것이 아니라 필터 대상이 된다. 실제 필터가 작동되게 하려면 Foo 클래스 내부에 @Defence 을 선언해야 한다.

@Defence
public class Foo { ...

클래스에 @Defence 을 선언하면 모두를 항목이 필터된다.

@Defence // (1)
public class Foo extends Too { ...

상속의 경우 (1)번과 같이 클래스에 선언하면 Foo Too 모두 필터가 된다. 만약 항목에 @Defence 을 선언한다면 자식 클래스 항목에도 @Defence 을 선언해야 필터된다. 단 자식 클래스는 클래스에 @Defence 를 선언할 수 없다. 꼭 항목에 선언해야 한다. (이부분은 추후 패치할 예정이다.) 부모 클래스에 @Defence 를 선언하고 자식 클래스 항목에 @Defence 를 선언하면 자식 클래스 항목의 @Defence 무시된다. 부모 클래스 @Defence 선언에 따라 필터된다.

public class Foo {
    @Defence
    private String name;
    @Defence
    private List<String> lists; // (1)
    @Defence
    private List<Too> toos; // (2)
    ...
    

클래스 항목에 @Defence 을 선언하면 해당 항목만 필터한다. (1)번의 경우 제너릭타입이 참조나 기본타입인 경우 필터가 되지만 (2)번 같이 클래스타입인 경우는 필터가 되는 것이 아니라 필터 대상이 되고 Too 클래스 내부에 @Defence 을 선언해야 한다.

적용하기

@Controller
@RequestMapping("/xss")
public class XssController {

    @GetMapping("")
    @ResponseBody
    public Map<String, String> dispView(
            @RequestParam(value = "html", required = false) @Defence(XssType.ESCAPE) String html) {
            
        Map<String, String> result = new HashMap<>();
        result.put("html", html);
        return result;
    }

    @PostMapping("/{idx}")
    @ResponseBody
    public Foo procPost(
            @Defence @RequestBody Foo foo,
            @Defence @PathVariable("idx") String idx) {
            
        foo.setIdx(idx);
        return foo;
    }
}

결과

테스트에 사용된 xss 코드

    String escape = "\"><script>alert('xss_');</script>";
    String filter = "<img src=\"<img src=1\\ onerror=alert(1234)>\" onerror=\"alert('XSS')\">";
    String saxFilter = "<TABLE class=\"NHN_Layout_Main\" style=\"TABLE-LAYOUT: fixed\" cellSpacing=\"0\" cellPadding=\"0\" width=\"743\">" + "</TABLE>" + "<SPAN style=\"COLOR: #66cc99\"><img src=\"<img src=1\\ onerror=alert(1234)>\" onerror=\"alert('XSS')\"></SPAN>";

필터 결과

{
  "escape": "&quot;&gt;&lt;script&gt;alert(&#39;xss_&#39;);&lt;/script&gt;",
  "doos": [
    {
      "saxFilter": "<TABLE class=\"NHN_Layout_Main\" style=\"TABLE-LAYOUT: fixed\" cellSpacing=\"0\" cellPadding=\"0\" width=\"743\"></TABLE><SPAN style=\"COLOR: #66cc99\"><img src=\"\"><img src=1\\>\" onerror=\"alert('XSS')\"&gt;</SPAN>",
      "noFilter": "<TABLE class=\"NHN_Layout_Main\" style=\"TABLE-LAYOUT: fixed\" cellSpacing=\"0\" cellPadding=\"0\" width=\"743\"></TABLE><SPAN style=\"COLOR: #66cc99\"><img src=\"<img src=1\\ onerror=alert(1234)>\" onerror=\"alert('XSS')\"></SPAN>"
    },
    
    ... skip ...
    
  ],
  "idx": "10000",
  "filter": "<img src=\"\"><img src=1\\>\" onerror=\"alert('XSS')\"&gt;",
  "toos": [
    {
      "saxFilter": "<TABLE class=\"NHN_Layout_Main\" style=\"TABLE-LAYOUT: fixed\" cellSpacing=\"0\" cellPadding=\"0\" width=\"743\"></TABLE><SPAN style=\"COLOR: #66cc99\"><img src=\"\"><img src=1\\>\" onerror=\"alert('XSS')\"&gt;</SPAN>",
      "noFilter": "<TABLE class=\"NHN_Layout_Main\" style=\"TABLE-LAYOUT: fixed\" cellSpacing=\"0\" cellPadding=\"0\" width=\"743\"></TABLE><SPAN style=\"COLOR: #66cc99\"><img src=\"<img src=1\\ onerror=alert(1234)>\" onerror=\"alert('XSS')\"></SPAN>"
    },
    
    ... skip ...
    
  ],
  "use": true
}