토리맘의 한글라이즈 프로젝트 logo 토리맘의 한글라이즈 프로젝트

스프링 시큐리티 공식 레퍼런스를 한글로 번역한 문서입니다.

전체 목차는 여기에 있습니다.

목차:


14.1. Cross Site Request Forgery (CSRF) for Servlet Environments

이번 섹션에선 서블릿 환경에서 스프링 시큐리티로 사이트간 요청 위조(Cross Site Request Forgery, CSRF)를 방어하는 방법을 설명한다.

14.1.1. Using Spring Security CSRF Protection

스프링 시큐리티로 CSRF를 방어하는 방법은 크게 다음 단계로 나뉜다:

Use proper HTTP verbs

CSRF 공격을 방어하려면 가장 먼저 만드려는 웹사이트에서 올바른 HTTP verb를 사용해야 한다. 자세한 내용은 Safe Methods Must be Idempotent에서 다루고 있다.

Configure CSRF Protection

그다음엔 어플리케이션에 스프링 시큐리티의 CSRF 방어 기능을 설정해야 한다. 스프링 시큐리티에선 CSRF 방어를 기본적으로 활성화하지만, 아마 일부 설정을 커스텀해야 할 거다. 아래는 일반적으로 커스텀하는 설정들이다.

Custom CsrfTokenRepository

기본적으로 스프링 시큐리티는 HttpSessionCsrfTokenRepository로 CSRF 토큰을 HttpSession에 저장한다. 하지만 커스텀 CsrfTokenRepository를 설정하고 싶을 때도 있을 것이다. 예를 들어 자바스크립트 기반 어플리케이션을 지원하려면 쿠키에 CsrfToken을 저장해야 한다.

기본적으로 CookieCsrfTokenRepositoryXSRF-TOKEN이란 쿠키를 저장하고, X-XSRF-TOKEN 헤더나 _csrf 파라미터에서 토큰을 읽어들인다. 이 디폴트 값들은 AngularJS에서 따온 값이다.

XML로 CookieCsrfTokenRepository를 설정하는 방법은 다음과 같다:

Example 117. Store CSRF Token in a Cookie with XML Configuration

<http>
    <!-- ... -->
    <csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
    class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
    p:cookieHttpOnly="false"/>

이 샘플에선 cookieHttpOnly=false를 명시했다. 자바스크립트(i.e. AngularJS)에서 읽어가려면 이렇게 명시해야 한다. 자바스크립트에서 직접 쿠키를 읽어야할 필요가 없다면 보안을 위해 cookieHttpOnly=false를 생략하는 게 좋다.

자바 설정으로 CookieCsrfTokenRepository를 설정하는 방법은 다음과 같다:

Example 118. Store CSRF Token in a Cookie with Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            );
    }
}

이 샘플에선 cookieHttpOnly=false를 명시했다. 자바스크립트(i.e. AngularJS)에서 읽어가려면 이렇게 명시해야 한다. 자바스크립트에서 직접 쿠키를 읽어야할 필요가 없다면 보안을 위해 cookieHttpOnly=false를 생략하는 게 좋다 (이대신 new CookieCsrfTokenRepository()를 사용해서).

Disable CSRF Protection

CSRF 방어는 기본적으로 활성화된다. 하지만 굳이 CSRF를 방어할 필요가 없는 어플리케이션을 만든다면 간단히 비활성화할 수 있다.

아래 XML 설정은 CSRF 방어를 비활성화한다.

Example 119. Disable CSRF XML Configuration

<http>
    <!-- ... -->
    <csrf disabled="true"/>
</http>

아래 자바 설정은 CSRF 방어를 비활성화한다.

Example 120. Disable CSRF Java Configuration

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            .csrf(csrf -> csrf.disable());
    }
}

Include the CSRF Token

동기화 토큰 패턴으로 CSRF 공격을 방어하려면 HTTP 요청에 실제 CSRF 토큰을 추가해야 한다. 토큰은 반드시 폼 파라미터, HTTP 헤더같이 브라우저가 HTTP 요청에 자동으로 추가하지 않는 곳에 넣어야 한다.

스프링 시큐리티의 CsrfFilterHttpServletRequest 속성에 _csrf라는 이름으로 CsrfToken을 추가한다. 따라서 어떻게 뷰를 렌더링하든지, 이나 메타 태그에서 CsrfToken으로 토큰에 접근할 수 있다. 아래에 있는 통합 기능 덕분에, formajax 요청에선 토큰을 더 쉽게 사용할 수 있다.

Form URL Encoded

HTML 폼을 사용한다면 hidden 인풋에 CSRF 토큰을 추가해야 한다. 예를 들어 다음과 같은 HTML을 렌더링할 수 있다:

Example 121. CSRF Token HTML

<input type="hidden"
    name="_csrf"
    value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>

이제부터는 CSRF 토큰을 hidden 인풋에 추가하는 여러 가지 방법을 설명한다.

Automatic CSRF Token Inclusion

스프링 시큐리티는 CsrfRequestDataValueProcessor로 스프링의 RequestDataValueProcessor와의 통합을 지원한다. 따라서 스프링의 폼 태그 라이브러리나, 타임리프RequestDataValueProcessor로 통합 지원하는 뷰 기술을 사용한다면, unsafe HTTP 메소드 (i.e. post)를 사용하는 폼은 자동으로 실제 CSRF 토큰을 추가할 것이다.

csrfInput Tag

JSP를 사용한다면 스프링의 폼 태그 라이브러리를 사용할 수 있다. 하지만 선택권이 없다면 간단하게 csrfInput 태그로 토큰을 추가해도 된다.

CsrfToken Request Attribute

다른 방법으로 요청에 실제 CSRF 토큰을 추가할 수 없다면, HttpServletRequest_csrf 속성에 CsrfToken이 들어있다는 점을 활용하면 된다.

다음은 JSP를 활용한 예시이다:

Example 122. CSRF Token in Form with Request Attribute

<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
    method="post">
<input type="submit"
    value="Log out" />
<input type="hidden"
    name="${_csrf.parameterName}"
    value="${_csrf.token}"/>
</form>
Ajax and JSON Requests

JSON을 사용한다면 HTTP 파라미터로 CSRF 토큰을 제출할 수 없다. 대신 HTTP 헤더를 활용할 수 있다.

아래 섹션에선 자바스크립트 기반 어플리케이션에서 HTTP 요청 헤더에 CSRF 토큰을 추가하는 여러 가지 방법을 설명한다.

Automatic Inclusion

간단하게 CSRF 토큰을 쿠키에 저장하도록 설정할 수 있다. CSRF 토큰을 쿠키에 저장하면 AngularJS같은 자바스크립트 프레임워크는 자동으로 HTTP 요청 헤더에 실제 CSRF 토큰을 추가한다.

Meta tags

쿠키에 CSRF 토큰을 추가하는 대신 사용할 수 있는 또 다른 패턴은 meta 태그를 사용하는 것이다. 다음과 같은 HTML이 있다면:

Example 123. CSRF meta tag HTML

<html>
<head>
    <meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
    <meta name="_csrf_header" content="X-CSRF-TOKEN"/>
    <!-- ... -->
</head>
<!-- ... -->

메타 태그에 CSRF 토큰을 추가하고 나면 자바스크립트 코드에서 이 메타 태그를 읽어 헤더에 CSRF 토큰을 추가할 수 있다. jQuery를 사용한다면 다음과 같이 작성할 수 있다:

Example 124. AJAX send CSRF Token

$(function () {
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function(e, xhr, options) {
        xhr.setRequestHeader(header, token);
    });
});
csrfMeta tag

JSP를 사용한다면 csrfMeta 태그를 활용해서 간단하게 CSRF 토큰을 meta 태그에 추가할 수 있다.

CsrfToken Request Attribute

다른 방법으로 요청에 실제 CSRF 토큰을 추가할 수 없다면, HttpServletRequest_csrf 속성에 CsrfToken이 들어있다는 점을 활용하면 된다. 다음은 JSP를 활용한 예시이다:

Example 125. CSRF meta tag JSP

<html>
<head>
    <meta name="_csrf" content="${_csrf.token}"/>
    <!-- default header name is X-CSRF-TOKEN -->
    <meta name="_csrf_header" content="${_csrf.headerName}"/>
    <!-- ... -->
</head>
<!-- ... -->

14.1.2. CSRF Considerations

CSRF 공격을 방어하려면 특별히 고려해야 할 몇 가지가 있다. 이번 섹션에선 서블릿 환경에서 주의할 사항을 설명한다. 일반적인 주의사항은 CSRF Considerations를 참고하라.

Logging In

로그인을 위조하지 못하게 막으려면 로그인 요청을 CSRF 공격으로부터 보호해야 한다. 스프링 시큐리티는 서블릿에서 CSRF 로그인 공격을 방어할 수 있는 기능을 지원한다.

Logging Out

로그아웃을 위조하지 못하게 막으려면 로그아웃 요청을 CSRF 공격으로부터 보호해야 한다. CSRF 방어를 활성화하면 (디폴트), 스프링 시큐리티의 LogoutFilter는 HTTP POST 요청만 처리한다. CSRF 토큰을 검사하면 악의적으로 다른 사용자의 로그아웃을 위조하지 못하게 막을 수 있다.

로그아웃을 구현하는 가장 쉬운 방법은 폼을 사용하는 것이다. 링크가 꼭 있어야 한다면 자바스크립트로 POST 요청을 수행하는 링크를 만들면 된다 (hidden 폼으로). 자바스크립트가 비활성화된 브라우저에선 POST 요청을 수행하는 로그아웃 컨펌 페이지로 사용자를 이동시키는 링크를 만들 수 있다.

로그아웃에서 HTTP GET을 꼭 써야 한다면 그럴 순 있지만, 일반적으로 권장하는 방법이 아니란 점을 알아둬라. 예를 들어 아래 자바 설정은 /logout URL의 모든 HTTP 메소드 요청에서 로그아웃을 수행한다.

Example 126. Log out with HTTP GET

@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            .logout(logout -> logout
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
            );
    }
}

CSRF and Session Timeouts

기본적으로 스프링 시큐리티에선 CSRF 토큰을 HttpSession에 저장한다. 따라서 세션이 만료되면 검증할 CSRF 토큰이 없는 상황이 발생할 수 있다.

세션 타임아웃을 해결하는 일반적인 방법은 앞에서 이미 설명했다. 이번에는 서블릿에서 CSRF 타임아웃에 대응하는 방법을 설명한다.

간단하게 CSRF 토큰을 쿠키에 저장하도록 바꿀 수 있다. 자세한 내용은 Custom CsrfTokenRepository 섹션을 참고하라.

토큰이 만료됐을 때의 처리를 커스텀한다면 커스텀 AccessDeniedHandler를 지정하면 된다. 커스텀 AccessDeniedHandler에서 원하는 방식으로 InvalidCsrfTokenException을 처리해라. AccessDeniedHandler를 커스텀하는 방법은 xml, 자바 설정 예제를 참고하라.

Multipart (file upload)

CSRF 공격으로부터 멀티파트 요청을 (파일 업로드) 보호하려고 할 땐 닭이 먼저인가, 달걀이 먼저인가하는 문제와 맞닥뜨릴 수 있다고 앞에서 이미 언급했었다. 이번 섹션에선 서블릿 어플리케이션에서 bodyurl에 CSRF 토큰을 추가하는 방법을 설명한다.

스프링에서 멀티파트 폼을 사용하는 방법은 스프링 레퍼런스의 1.1.11. Multipart Resolver 섹션과 MultipartFilter javadoc에서 자세히 설명한다.

Place CSRF Token in the Body

CSRF 토큰을 바디에 두면 생길 수 있는 상황은 앞에서 이미 설명했다. 이번 섹션은 스프링 시큐리티에서 CSRF 토큰을 body에서 읽도록 설정하는 방법을 설명한다.

body에서 CSRF 토큰을 읽으려면 MultipartFilter를 스프링 시큐리티 필터보다 앞에 선언해야 한다. 스프링 시큐리티 필터보다 MultipartFilter가 앞에 있으면, MultipartFilter를 실행할 땐 권한을 검사하지 않으므로 누구든지 서버에 임시 파일을 생성할 수 있다는 뜻이다. 하지만 어플리케이션은 권한이 있는 사용자가 제출한 파일만 처리할 것이다. 임시 파일 업로드가 서버에 주는 영향은 거의 무시해도 될 수준이기 때문에, 일반적으로 권장하는 방법이다.

자바 설정에서 MultipartFilter를 스프링 시큐리티 필터보다 앞에 지정하려면 아래처럼 beforeSpringSecurityFilterChain을 재정의하면 된다:

Example 127. Initializer MultipartFilter

public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

    @Override
    protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
        insertFilters(servletContext, new MultipartFilter());
    }
}

XML 설정에서 MultipartFilter를 스프링 시큐리티 필터보다 앞에 지정하려면, 아래처럼 web.xml 파일에서 MultipartFilter의 <filter-mapping> 요소를 springSecurityFilterChain보다 앞에 두면 된다:

Example 128. web.xml - MultipartFilter

<filter>
    <filter-name>MultipartFilter</filter-name>
    <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>MultipartFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
Include CSRF Token in URL

권한이 없는 사용자가 임시 파일을 업로드하게 만드는 게 불가능하다면, MultipartFilter를 스프링 시큐리티 필터 뒤에 두고, 폼의 action 속성에 쿼리 파라미터로 CSRF 토큰을 넣을 수도 있다. HttpServletRequest 요청 속성CsrfToken이 들어있으므로, 이를 사용해서 action을 만들 수 있다. 다음은 jsp를 사용한 예시이다.

Example 129. CSRF Token in Action

<form method="post"
    action="./upload?${_csrf.parameterName}=${_csrf.token}"
    enctype="multipart/form-data">

HiddenHttpMethodFilter

CSRF 토큰을 바디에 두면 생길 수 있는 상황은 앞에서 이미 설명했다.

서블릿 기반 어플리케이션에서 스프링을 사용한다면 HiddenHttpMethodFilter로 HTTP 메소드를 재정의할 수 있다. 자세한 정보는 레퍼런스 문서 HTTP Method Conversion 섹션을 참고하라.


14.2. Security HTTP Response Headers

보안 HTTP 응답 헤더는 웹 어플리케이션을 보호할 때 사용하는 헤더다. 이번 섹션에선 서블릿 기반 어플리케이션에서 사용할 수 있는 보안 HTTP 응답 헤더를 설명한다.

14.2.1. Default Security Headers

스프링 시큐리티는 기본적인 보안을 위한 HTTP 응답 헤더의 디폴트 셋을 제공한다. 모든 헤더가 다 좋은 관행이라고는 하지만, 그렇다고 해서 모든 클라이언트가 다 헤더를 사용하는 것은 아니므로 추가적인 테스트가 필요하다.

특정 헤더를 커스텀할 수도 있다. 예를 들어 X-Frame-Options에선 SAMEORIGIN을 지정하돼, 다른 헤더는 모두 디폴트 값을 사용하고 싶을 수도 있다.

아래처럼 자바 설정으로 쉽게 변경할 수 있다:

Example 130. Customize Default Security Headers with Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            // ...
            .headers(headers -> headers
                .frameOptions(frameOptions -> frameOptions
                    .sameOrigin()
                )
            );
    }
}

스프링 시큐리티 XML 설정을 사용하고 있다면 다음과 같이 변경해라:

Example 131. Customize Default Security Headers with XML Configuration

<http>
    <!-- ... -->

    <headers>
        <frame-options policy="SAMEORIGIN" />
    </headers>
</http>

디폴트 값을 사용하는 대신 사용할 헤더만 명시하고 싶다면 디폴트 설정을 비활성화할 수 있다. 자바와 XML 기반 설정 예시 모두 아래에 있다:

스프링 시큐리티의 자바 설정을 사용한다면 다음과 같이 Cache Control만 추가할 수 있다.

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                // do not use any default headers unless explicitly listed
                .defaultsDisabled()
                .cacheControl(withDefaults())
            );
    }
}

아래 XML도 Cache Control만 추가한다.

<http>
    <!-- ... -->

    <headers defaults-disabled="true">
        <cache-control/>
    </headers>
</http>

필요하다면 아래 자바 설정으로 모든 HTTP 보안 응답 헤더를 비활성화할 수 있다:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers.disable());
    }
}

필요하다면 아래 XML 설정으로 모든 HTTP 보안 응답 헤더를 비활성화할 수 있다:

<http>
    <!-- ... -->

    <headers disabled="true" />
</http>

14.2.2. Cache Control

스프링 시큐리티는 디폴트로 Cache Control 헤더를 추가한다.

하지만 특정 응답을 캐시하고 싶다면 어플리케이션에서 원하는 곳에서 HttpServletResponse.setHeader(String,String)을 호출해서 스프링 시큐리티의 헤더 셋을 재정의하면 된다. CSS, 자바스크립트, 이미지 등을 적절히 캐시해야 할 때 유용하다.

스프링 웹 MVC를 사용한다면 설정으로 캐시를 관리할 수 있다. 자세한 방법은 스프링 레퍼런스 문서의 스태틱 리소스 섹션을 참고해라.

필요하다면 스프링 시큐리티의 cache control HTTP 응답 헤더를 비활성화할 수 있다.

Example 132. Cache Control Disabled with Java Configuration

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            // ...
            .headers(headers -> headers
                .cacheControl(cache -> cache.disable())
            );
    }
}

유사하게 <cache-control> 요소로도 비활성화할 수 있다:

Example 133. Cache Control Disabled with XML

<http>
    <!-- ... -->

    <headers>
        <cache-control disabled="true"/>
    </headers>
</http>

14.2.3. Content Type Options

스프링 시큐리티는 디폴트로 Content-Type 헤더를 추가한다. 하지만 자바 설정으로 비활성화할 수 있다:

Example 134. Content Type Options Disabled with Java Configuration

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            // ...
            .headers(headers -> headers
                .contentTypeOptions(contentTypeOptions -> contentTypeOptions.disable())
            );
    }
}

유사하게 <content-type-options> 요소로도 비활성화할 수 있다:

Example 135. Content Type Options Disabled with XML

<http>
    <!-- ... -->

    <headers>
        <content-type-options disabled="true"/>
    </headers>
</http>

14.2.4. HTTP Strict Transport Security (HSTS)

스프링 시큐리티는 디폴트로 Strict Transport Security 헤더를 추가한다. 하지만 헤더 내용을 직접 커스텀할 수도 있다. 예를 들어 다음 예제는 자바 설정으로 사용할 HSTS를 지정한다:

Example 136. Strict Transport Security with Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .httpStrictTransportSecurity(hsts -> hsts
                    .includeSubDomains(true)
                    .preload(true)
                    .maxAgeInSeconds(31536000)
                )
            );
    }
}

유사하게 XML 설정에선 아래처럼 <hsts> 요소로 HSTS를 설정할 수 있다:

Example 137. Strict Transport Security with XML Configuration

<http>
    <!-- ... -->

    <headers>
        <hsts
            include-subdomains="true"
            max-age-seconds="31536000"
            preload="true" />
    </headers>
</http>

14.2.5. HTTP Public Key Pinning (HPKP)

스프링 시큐리티는 하위 호환성을 위해 서블릿 환경에서 HTTP Public Key Pinning을 지원하고 있지만, 더 이상 HPKP 사용을 권장하지 않는다.

자바 설정에선 아래와 같이 HPKP 헤더를 활성화할 수 있다:

Example 138. HTTP Public Key Pinning with Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .httpPublicKeyPinning(hpkp -> hpkp
                    .includeSubDomains(true)
                    .reportUri("https://example.net/pkp-report")
                    .addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=")
                )
            );
    }
}

유사하게 <hpkp> 요소로도 HPKP 헤더를 활성화할 수 있다:

Example 139. HTTP Public Key Pinning with XML Configuration

<http>
    <!-- ... -->

    <headers>
        <hpkp
            include-subdomains="true"
            report-uri="https://example.net/pkp-report">
            <pins>
                <pin algorithm="sha256">d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=</pin>
                <pin algorithm="sha256">E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=</pin>
            </pins>
        </hpkp>
    </headers>
</http>

14.2.6. X-Frame-Options

스프링 시큐리티는 기본적으로 X-Frame-Options를 사용해서 아이프레임 내부 렌더링을 비활성화한다.

다음 코드는 자바 설정으로 프레임 옵션을 SAMEORIGIN으로 커스텀한다:

Example 140. X-Frame-Options: SAMEORIGIN with Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .frameOptions(frameOptions -> frameOptions
                    .sameOrigin()
                )
            );
    }
}

XML 설정에선 <frame-options> 요소를 사용한다:

Example 141. X-Frame-Options: SAMEORIGIN with XML Configuration

<http>
    <!-- ... -->

    <headers>
        <frame-options
        policy="SAMEORIGIN" />
    </headers>
</http>

14.2.7. X-XSS-Protection

스프링 시큐리티는 기본적으로 X-XSS-Protection 헤더를 사용해서 브라우저에게 reflected XSS 공격을 막도록 지시한다. 하지만 이 디폴트 설정을 바꿀 수도 있다. 예를 들어 스프링 시큐리티는 다음 자바 설정에선 브라우저에게 더 이상 컨텐츠를 막으라고 지시하지 않는다:

Example 142. X-XSS-Protection Customization with Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .xssProtection(xss -> xss
                    .block(false)
                )
            );
    }
}

유사하게 다음 XML 설정도 브라우저에게 더 이상 컨텐츠를 막으라고 지시하지 않는다:

Example 143. X-XSS-Protection Customization with XML Configuration

<http>
    <!-- ... -->

    <headers>
        <xss-protection block="false"/>
    </headers>
</http>

14.2.8. Content Security Policy (CSP)

컨텐츠 보안 정책은 어플리케이션에 따라 다르므로 디폴트라는 게 불가능하며, 스프링 시큐리티는 디폴트로 관련 헤더를 추가하지 않는다. 보안 정책을 운영하거나 보호 중인 리소스를 모니터링하려면 어플리케이션에서 직접 지정해야 한다.

예를 들어 다음과 같은 보안 정책을 사용한다면:

Example 144. Content Security Policy Example

Content-Security-Policy: script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/

아래에 있는 자바 설정으로 CSP 헤더를 활성화할수 있다:

Example 145. Content Security Policy Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            // ...
            .headers(headers -> headers
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/")
                )
            );
    }
}

XML 설정에선 <content-security-policy> 요소를 활용한다:

Example 146. Content Security Policy Java Configuration

<http>
    <!-- ... -->

    <headers>
        <content-security-policy
            policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/" />
    </headers>
</http>

CSP report-only 헤더를 사용하려면 아래 자바 설정을 사용해라:

Example 147. Content Security Policy Report Only Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/")
                    .reportOnly()
                )
            );
    }
}

이와 동일한 XML 설정은 다음과 같다:

Example 148. Content Security Policy XML Configuration

<http>
    <!-- ... -->

    <headers>
        <content-security-policy
            policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/"
            report-only="true" />
    </headers>
</http>

14.2.9. Referrer Policy

스프링 시큐리티는 디폴트로 Referrer Policy 헤더를 추가하지 않는다. 아래와 같은 자바 설정으로 Referrer Policy 헤더를 추가할 수 있다:

Example 149. Referrer Policy Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            // ...
            .headers(headers -> headers
                .referrerPolicy(referrer -> referrer
                    .policy(ReferrerPolicy.SAME_ORIGIN)
                )
            );
    }
}

XML 설정에선 <referrer-policy> 요소로 Referrer-Policy 헤더를 추가한다:

Example 150. Referrer Policy XML Configuration

<http>
    <!-- ... -->

    <headers>
        <referrer-policy policy="same-origin" />
    </headers>
</http>

14.2.10. Feature Policy

스프링 시큐리티는 디폴트로 Feature Policy 헤더를 추가하지 않는다. 다음 Feature-Policy 헤더는:

Example 151. Feature-Policy Example

Feature-Policy: geolocation 'self'

아래 자바 설정으로 활성화할 수 있다:

Example 152. Feature-Policy Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .featurePolicy("geolocation 'self'")
            );
    }
}

XML 설정에선 <feature-policy> 요소로 Feature-Policy 헤더를 활성화한다:

Example 153. Feature-Policy XML Configuration

<http>
    <!-- ... -->

    <headers>
        <feature-policy policy-directives="geolocation 'self'" />
    </headers>
</http>

14.2.11. Clear Site Data

스프링 시큐리티는 디폴트로 Clear-Site-Data 헤더를 추가하지 않는다. 아래 Clear-Site-Data 헤더는:

Example 154. Clear-Site-Data Example

Clear-Site-Data: "cache", "cookies"

아래 자바 설정을 사용해서 로그아웃 시 전송할 수 있다:

Example 155. Clear-Site-Data Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .logout()
                .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(CACHE, COOKIES)));
    }
}

14.2.12. Custom Headers

스프링 시큐리티는 좀 더 일반적인 보안 헤더를 어플리케이션에 편리하게 추가할 수 있는 메커니즘을 제공한다. 하지만 커스텀 헤더를 추가할 수 있는 훅도 함께 제공한다.

Static Headers

스프링 시큐리티가 지원하지 않는 커스텀 보안 헤더를 주입하고 싶을 때도 있을 것이다. 예를 들어 아래 커스텀 보안 헤더를 사용한다면:

X-Custom-Security-Header: header-value

아래 자바 설정으로 응답에 이 헤더를 추가할 수 있다:

Example 156. StaticHeadersWriter Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .addHeaderWriter(new StaticHeadersWriter("X-Custom-Security-Header","header-value"))
            );
    }
}

XML 네임스페이스를 사용한다면 아래와 같이 <header> 요소로 응답 헤더를 추가한다:

Example 157. StaticHeadersWriter XML Configuration

<http>
    <!-- ... -->

    <headers>
        <header name="X-Custom-Security-Header" value="header-value"/>
    </headers>
</http>

Headers Writer

네임스페이스나 자바 설정으로 원하는 헤더를 만들 수 없다면 원하는 HeadersWriter 인스턴스를 생성하거나 직접 HeadersWriter를 구현해도 된다.

XFrameOptionsHeaderWriter 인스턴스를 생성하는 예제를 살펴보자. X-Frame-Options를 직접 설정하려면 아래 자바 설정을 사용하면 된다:

Example 158. Headers Writer Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN))
            );
    }
}

XML 설정에선 ref 속성을 사용하면 된다:

Example 159. Headers Writer XML Configuration

<http>
    <!-- ... -->

    <headers>
        <header ref="frameOptionsWriter"/>
    </headers>
</http>
<!-- Requires the c-namespace.
See https://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#beans-c-namespace
-->
<beans:bean id="frameOptionsWriter"
    class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter"
    c:frameOptionsMode="SAMEORIGIN"/>

DelegatingRequestMatcherHeaderWriter

특정 요청에만 헤더를 추가하고 싶을 때도 있다. 예를 들어 로그인 페이지만 프레임에 넣지 못하게 막을 수 있다. 이럴 땐 DelegatingRequestMatcherHeaderWriter를 사용하면 된다.

다음은 자바 설정에서 DelegatingRequestMatcherHeaderWriter를 사용하는 예제다:

Example 160. DelegatingRequestMatcherHeaderWriter Java Configuration

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        RequestMatcher matcher = new AntPathRequestMatcher("/login");
        DelegatingRequestMatcherHeaderWriter headerWriter =
            new DelegatingRequestMatcherHeaderWriter(matcher,new XFrameOptionsHeaderWriter());
        http
            // ...
            .headers(headers -> headers
                .frameOptions(frameOptions -> frameOptions.disable())
                .addHeaderWriter(headerWriter)
            );
    }
}

다음은 이와 동일한 XML 설정이다:

Example 161. DelegatingRequestMatcherHeaderWriter XML Configuration

<http>
    <!-- ... -->

    <headers>
        <frame-options disabled="true"/>
        <header ref="headerWriter"/>
    </headers>
</http>

<beans:bean id="headerWriter"
    class="org.springframework.security.web.header.writers.DelegatingRequestMatcherHeaderWriter">
    <beans:constructor-arg>
        <bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher"
            c:pattern="/login"/>
    </beans:constructor-arg>
    <beans:constructor-arg>
        <beans:bean
            class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter"/>
    </beans:constructor-arg>
</beans:bean>

14.3. HTTP

모든 HTTP 통신은 TLS로 보호해야 한다.

아래에선 서블릿 어플리케이션에서 HTTPS를 사용할 때 활용할만한 기능을 설명한다.

14.3.1. Redirect to HTTPS

스프링 시큐리티는 클라이언트가 HTTPS가 아닌 HTTP로 보낸 요청을 HTTPS로 리다이렉트하게 설정할 수 있다.

예를 들어 아래 자바 설정은 모든 HTTP 요청을 HTTPS로 리다이렉트한다:

Example 162. Redirect to HTTPS with Java Configuration

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            // ...
            .requiresChannel(channel -> channel
                .anyRequest().requiresSecure()
            );
    }
}

아래는 모든 HTTP 요청을 HTTPS로 리다이렉트하는 XML 설정이다:

Example 163. Redirect to HTTPS with XML Configuration

<http>
    <intercept-url pattern="/**" access="ROLE_USER" requires-channel="https"/>
...
</http>

14.3.2. Strict Transport Security

스프링 시큐리티는 Strict Transport Security를 지원하며 디폴트로 활성화돼 있다.

14.3.3. Proxy Server Configuration

스프링 시큐리티는 프록시 서버와의 통합을 지원한다.


14.4. HttpFirewall

스프링 시큐리티엔 요청을 어떻게 처리할지 결정하기 위해, 들어온 요청이 정의한 패턴과 일치하는지 테스트하는 영역이 있다. FilterChainProxy가 요청을 전달할 필터 체인을 결정할 때와, FilterSecurityInterceptor가 요청에 적용할 보안 제약 조건을 결정할 때가 그렇다. 따라서 패턴을 비교할 때 사용하는 메커니즘과 URL을 이해하고 있어야 한다.

서블릿 스펙은 getter 메소드로 접근할 수 있는 몇 가지 HttpServletRequest 속성을 정의하고 있으며, 이 속성으로 패턴을 비교한다. 이 속성은 바로 contextPath, servletPath, pathInfo, queryString이다. 스프링 시큐리티는 어플리케이션에서 보호 중인 path만 알면 되기 때문에 contextPath는 무시한다. 안타깝게도 서블릿 스펙은 servletPathpathInfo가 정확하게 어떤 요청 URI를 가지고 있는지는 정의하지 않는다. 예를 들어 RFC 2396에 정의된대로 각 URL의 path segment는 파라미터를 포함할 수도 있다. 이 스펙은 servletPathpathInfo에 파라미터가 포함돼야 하는지 정확하게 진술하지 않으며, 실제 동작은 서블릿 컨테이너마다 다르다. 컨테이너로 배포한 어플리케이션이 이 값에서 path 파라미터를 제거하지 않으면, 공격자가 요청 URL에 파리미터를 추가해서 어플리케이션에서 의도한 것과는 달리 패턴에 매치하거나 매치하지 않게 만들 수 있다. 다른식으로 요청 URL을 변형하는 것도 가능하다. 예를 들어 /../같이 디렉토리를 순회하거나 (path-traversal), //같이 슬래시를 여러개 사용해서 (multiple forward slashes) 패턴 비교를 피해갈 수 있다. 서블릿에 매핑하기 전에 이 값을 보정하는 컨테이너도 있지만, 그렇지 않은 컨테이너도 있다. 이런 이슈를 피하려면 FilterChainProxy에서 요청을 확인하고 래핑하는 HttpFirewall 전략을 사용해야 한다. 기본적으로 보정되지 않은 비정상 요청은 자동으로 거부하며, path 파라미터나 중복 슬래시는 제거하고 비교한다. 따라서 필수로 FilterChainProxy를 사용해서 보안 필터 체인을 관리해야 한다. servletPath, pathInfo 값은 컨테이너가 디코딩하므로, 어플리케이션에서 사용하는 path는 패턴 비교를 위해 삭제시키는 세미콜론을 가지고 있으면 안된다.

위에서 언급한대로 패턴 매칭에 사용하는 디폴트 전략은 Ant-style path이며, 대부분 이 전략이 가장 잘 맞을 거다. 구현체는 AntPathRequestMatcher 클래스이며, 스프링의 AntPathMatcher를 사용해 패턴을 비교한다. 대소문자는 구분하지 않으며, queryString은 무시하고 servletPathpathInfo만 비교한다.

어떠한 이유로 좀 더 강력한 매칭 전략이 필요하다면 정규식을 사용할 수 있다. 구현체는 RegexRequestMatcher다. 이 클래스에 대한 자세한 정보는 Javadoc을 참고해라.

실제로는 서비스 레이어에 메소드 시큐리티를 사용해서 어플리케이션 접근을 제어하는 것이 좋으며, 웹 어플리케이션 레벨에 정의한 보안 제약 조건에 전적으로 의존하지 않는 것이 좋다. URL은 언제든지 변경될 수 있으며, 어플리케이션에서 지원하는 모든 URL 요청을 처리하는 방식을 전부 고려하기는 힘들다. ant path는 이해하기 쉬운 간단한 것으로 몇 가지만 사용하는 게 좋다. URL별로 접근을 제어할 때는 항상 마지막에 catch-all 와일드카드를 설정해서( / or ) 접근을 막아버리는 “deny-by-default” 방식을 사용해라.

보안 규칙을 서비스 레이어에 정의하는 게 훨씬 더 견고하고 우회하기도 어렵기 때문에, 항상 스프링 시큐리티의 메소드 시큐리티를 활용하는 게 좋다.

HttpFirewall은 HTTP 응답 헤더에 줄바꿈 문자를 허용하지 않으므로 HTTP 응답 분할(HTTP Response Splitting)도 방어할 수 있다.

StrictHttpFirewall은 디폴트로 사용된다. 이 구현체는 악의적인 요청으로 판단하면 요청을 거부한다. 요구사항에 비해 지나치게 깐깐하다면 거부할 요청 유형을 커스텀할 수 있다. 하지만 이렇게하면 어플리케이션이 공격에 노출될 수도 있음을 알고 있어야 한다. 예를 들어 스프링 MVC의 메트릭스 변수를 사용하고 싶다면 아래 설정을 사용할 수 있다:

Example 164. Allow Matrix Variables

java xml kotlin
@Bean
public StrictHttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowSemicolon(true);
    return firewall;
}
<b:bean id="httpFirewall"
    class="org.springframework.security.web.firewall.StrictHttpFirewall"
    p:allowSemicolon="true"/>

<http-firewall ref="httpFirewall"/>
@Bean
fun httpFirewall(): StrictHttpFirewall {
    val firewall = StrictHttpFirewall()
    firewall.setAllowSemicolon(true)
    return firewall
}

StrictHttpFirewall은 허용할 HTTP 메소드 리스트를 제공해서 Cross Site Tracing (XST)HTTP Verb Tampering을 방어한다. 디폴트로 허용하는 메소드는 “DELETE”, “GET”, “HEAD”, “OPTIONS”, “PATCH”, “POST”, “PUT”이다. 유효한 메소드를 변경하고 싶다면 커스텀 StrictHttpFirewall 빈을 설정하면 된다. 예를 들어 아래 예제는 HTTP “GET”, “POST” 메소드만 허용한다:

Example 165. Allow Only GET & POST

java xml kotlin
@Bean
public StrictHttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));
    return firewall;
}
<b:bean id="httpFirewall"
      class="org.springframework.security.web.firewall.StrictHttpFirewall"
      p:allowedHttpMethods="GET,HEAD"/>

<http-firewall ref="httpFirewall"/>
@Bean
fun httpFirewall(): StrictHttpFirewall {
    val firewall = StrictHttpFirewall()
    firewall.setAllowedHttpMethods(listOf("GET", "POST"))
    return firewall
}

new MockHttpServletRequest()를 사용하면 현재는 HTTP 메소드를 빈 문자열 ““로 만든다. 이 값은 유효한 HTTP 메소드가 아니므로 스프링 시큐리티는 요청을 거부한다. new MockHttpServletRequest("GET", "")을 사용하면 이 문제를 해결할 수 있다. 개선을 요청했던 이슈 SPR_16851을 참고하라.

모든 HTTP 메소드를 다 허용해야 한다면 (권장하지 않음), StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true)를 사용해라. 이 메소드는 HTTP 메소드 검증을 완전히 비활성화한다.


Next :
Integrations
스프링 시큐리티를 서블릿 API, spring data, spring mvc, websocket 등과 통합하는 방법을 설명합니다. 공식 문서에 있는 "integrations" 챕터를 한글로 번역한 문서입니다.

전체 목차는 여기에 있습니다.

<< >>

TOP