오래만에 공부좀 해볼까

이 글은 다음 원글을 참고, 번역, 편역, 요약한 글이오니 보다 정확한 정보 습득을 원하시는 분께서는 원문을 참고하시기 바랍니다: http://blog.springsource.org/2013/08/21/spring-security-3-2-0-rc1-highlights-csrf-protection/

스프링 시큐리티가 지난 월요일 3.2.0.RC1으로 버전이 올라갔는데 이번 글에서는 그 기능 중에 CSRF 지원 기능을 살펴보고자 한다. 다음글에서는 이번에 추가한 다양한 시큐리티 헤더를 살펴보겠다.

CSRF 공격

스프링 시큐리티가 CSRF 공격에 대한 방어책을 마련했다는데 CSRF 공격이 뭐고 그걸 어떻게 방어하겠다는걸까? 예제를 사용해 이해해보자.

여러분의 은행 웹 사이트가 현재 로그인한 사용자가 다른 은행으로 돈을 보낼 수 있는 폼을 제공한다고 가정해보자. 예를 들어 그 HTTP 요청은 다음과 같이 생겼다고 하자.

```

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876

```

이제 여러분이 은행 웹사이트에 인증을 하고나서 로그아웃하지 않고 다른 나쁜 웹사이트에 접속한다고 가정해보자. 그 나쁜 웹사이트는 다음과 같은 폼을 가진 HTML 페이지를 제공한다.

```
<form action="https://bank.example.com/transfer" method="post">
<input type="hidden"
name="amount"
value="100.00"/>
<input type="hidden"
name="routingNumber"
value="evilsRoutingNumber"/>
<input type="hidden"
name="account"
value="evilsAccountNumber"/>
<input type="submit"
value="Win Money!'/>
</form>

```

Win Money!를 하고 싶어서 서브밋 버튼을 클릭한 순간, 전혀 의도하지 않았던 100달러를 이상한 사람에게 보내게된다. 나쁜 사이트가 여러분의 쿠키를 보지 않더라도, 여러분 은행과 관련있는 쿠키가 요청을 따라 보내지기 때문에 이런 일이 발생한다.

더 안 좋은건 이 모든 절차를 자바스크립트로 자동화해서 실행할 수 있다는 것이다. 즉, 버튼을 클릭하지 않아도 이런 일이 생길 수 있다는 것이다. 그래서 이걸 어떻게 막았냐고?

SYNCHRONIZER TOKEN 패턴

은행 웹사이트의 HTTP 요청 폼과 나쁜 사이트의 요청 폼이 정확히 일치한다는 것이 문제다. 즉 나쁜 사이트에서 보내는 요청은 막으면서 은행에서 온 요청만 받을 방법은 없다는 뜻이다. CSRF 공격을 방어하려면 나쁜 사이트는 주지 못할 무언가가 요청 안에 들어있다는 것을 확인할 수 있어야 한다.

그런 대안 중 하나로 Synchronizer Token 패턴을 사용하는 방법이 있다. 이 방법은 모든 요청에 세션 쿠키와 더불어 랜덤하게 생성되는 토큰을 HTTP 파라메터로 제공하는 것이다. 요청이 오면, 서버는 반드시 그 토큰에 해당하는 값을 가져와서 요청에 있는 실제 값과 비교한다. 값이 맞지 않으면 그 요청은 실패 처리한다.

상태를 변경하는 HTTP 요청에서만 토큰을 사용하도록하여 이런 기대감을 조금은 완화할 수 있겠다. same origin policy로 인해 나쁜 사이트가 응답을 읽어가진 못할테니 그렇게 해도 비교적 안전하다. (즉, READ는 same origin policy로 막으니까 UPDATE만 sychronized token 패턴으로 막아도 된단 소리) 게다가, 랜덤 토큰을 HTTP GET에 넣는건 토큰이 누출될 가능성도 있다.

예제가 어떻게 바뀌는지 살펴보자. 랜덤하게 생성된 토큰을 _csrf라는 HTTP 파라메터로 넣는다고 가정하자. 가령, 송금 요청은 다음과 같다.

```

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876&_csrf=<secure-random>

```

임의의 값을 가진 _csrf 파라메터를 추가한걸 볼 수 있다. 이제 나쁜 사이트는 _csrf 파라메터에 적절한 값을 추측할 수 없으니 서버가 실제 토큰과 기대한 토큰을 비교해보고 송금 요청은 실패하게 된다.

스프링 시큐리티 CSRF 기능 사용하기

그래서 스프링 시큐리티로 웹 사이트를 CSRF 공격으로부터 방어하려면 뭘 해야하냐? 다음 단계를 통해 스프링 시큐리티 CSRF 방어를 사용할 수 있다.

  • 적절한 HTTP 동사 사용하기
  • CSRF 방어 설정하기
  • CSRF 토큰 추가하기

적절한 HTTP 동사 사용하기

CSRF 공격을 방어하는 첫걸음은 웹사이트가 적절한 HTTP 동사를 사용하고 있는지 확인하는 것이다. 구체적으로, 스프링 시큐리티 CSRF 기능을 사용하기 전에, 상태를 변경하는 요청일때 PATCH, POST, PUT, DELETE를 적절히 사용하고 있는지 확인해야한다. 이것은 스프링 시큐리티의 제약사항이 아니라 적절한 CSRF 방어에 기본으로 필요한 것이다.

CSRF 방어 설정하기

다음 단계는 스프링 시큐리티의 CSRF 방어를 애플리케이션에 추가하는 것이다.  XML 설정을 사용하고 있다면, <csrf/> 엘리먼트를 사용하면 된다.

```

<http ...>
...
<csrf />
</http>

```

CSRF 방어는 자바 설정에서 기본으로 사용하게된다. 궁금해 하실 분들을 위해 보여주자면, 자바 설정은 다음과 같이 생겼다.

```

@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.and()
...;
}
}

```

CSRF 토큰 추가하기

마지막 단계는 CSRF 토큰을 PATCH, POST, PUT, DELETE 메서드에 추가했는지 확인하는 것이다. _csrf 라는 요청 애트리뷰트를 사용해서 현재 CsrfToken을 가져와 사용할 수 있다. JSP에서 사용하는 예제는 다음과 같다.

```

<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>

```

스프링 MVC가 제공하는 <form:form> 태그를 사용하면 CsrfRequestDataValueProcessor를 사용하여 CsrfToken이 자동으로 들어간다.

CSRF 주의할 것

CSRF를 적용할 때 몇가지 주의할 것이 있다.

타임아웃

기대했던 CSRF 토큰이 HttpSession에 저장되기 때문에 HttpSession이 만려되면  AccessDeniedHadnelr가 InvalidCsrfTokenException을 박데된다. 기본 AccessDeniedHandler를 사용하면 브라우저는 HTTP 403을 받게되고 조악한 에러 메시지를 보여줄 것이다.

실제 사용자 경험을 조금 더 완화 시킬 수 있는 간단한 방법으로는 자바스크립트를 사용하여 사용자에게 세션이 만료됐다는 사실을 사용자에게 알려주는 것이다. 사용자는 버튼을 클릭해서 세션을 "계속" 하거나 "갱신" 할 수 있다.

또 다른 대안으로는, 커스텀한 AccessDeniedHandler를 제공하여 InvalidCsrfTokenException을 원하는 방법으로 다루는 것이다. AccessDeniedHandler를 커스터마이징하는 방법은 XML이나 자바 설정으로 제공된 링크를 참고하도록 하자.

CSRF를 쿠키에 넣는건 어때?

다른 도메인이 쿠키를 세팅할 수 있기도 하고, 뭔가 위험해졌을 때 강제적으로 토큰을 제거하지도 못한다는 담점이 있다.

로그인

강제 로그인 요청을 방어하려면 로그인 폼도 CSRF 공겨을 방어해야한다. CsrfToken이  HttpSession에 저장되니까, HttpSession이 그 즉시 생성되야 한다는걸 뜻한다. 이 말은RESTful이나  stateless 아키텍처에는 안좋게 들리겠지만, 현실적으로 실질적인 보안책을 구현하려면 상태를 필요로 한다. 상태 없이는 토큰이 정상인지 확인할 방법이 없다. 현실적으로 말해서, CSRF 토큰은 매우 작은 크기이고 아키텍처에 끼치는 영향은 무시해도 될정도의 수준으로 그쳐야 한다.

로그아웃

CSRF를 사용하면 LogoutFilter가 HTTP POST만 사용하도록 바뀐다. 로그아웃할 때 CSRF 토큰을 사용하고 나쁜 사용자가 다른 사용자를 강제로 로그아웃 시킬 수 없게 한다.

한가지 방법은 로그아웃할 때 폼을 사용하는 것이다. 링크를 사용하고 싶다면 자바스크립트로 POST를 수행하는 링크를 사용하자(히든 폼으로). 자바스크립트를 사용하지 않는 브라우저에서는 선별적으로 POST 요청을 보내는 로그아웃 확인 페이지로 이동시킬 수 있겠다.

HiddenHttpMethodFilter

이 필터가 스프링 시큐리티 필터보다 먼저 등록되어 있어야 한다. In general this is true, but it could have additional implications when protecting against CSRF attacks.

Note that the HiddenHttpMethodFilter only overrides the HTTP method on a POST, so this is actually unlikely to cause any real problems. However, it is still best practice to ensure it is placed before Spring Security's filters.

나머진 패스.

기본 설정 덮어쓰기

스프링 시큐리티의 목적은 사용자 폼이 노출되는 걸 막는 기본값을 제공하는 것이다. 그렇다고 해서 모두가 그 기본값을 사용하도록 강요하려는 것은 아니다.

가령, 커스텀한 CsrfTokenRepository를 제공하여 CsrfToken이 저장되는 곳을 변경할 수있다.

또한, 커스텀한 RequestMatcher를 설정하여 어떤 요청을 CSRF로 방어할지 결정할 수 있다(예를 들어, 로그아웃은 적용할 필요가 없다던지). 즉, 만약 스프링 시큐리티의 CSRF 방어가 여러분이 원하는 것과 정확히 맞지 않다면 여러분이 그 동작을 커스터마이징 할 수 있단 말이다.

결론

이제 CSRF가 뭔지 이해하고 스프링 시큐리티를 사용해서 애플리케이션을 CSRF 공격으로부터 어떻게 방어하는지 이해했을 것이다.

다음 글에서는 스프링 시큐리티의 헤더 기능을 사용해서 clickjacking 같은 공격으로부터 애플리케이션을 보호하는 방법을 살펴보겠다.

 

흥미진진 하구만~