스프링 시큐리티 3.2.0.RC1 하이라이트: CSRF 방어

오래만에 공부좀 해볼까

이 글은 다음 원글을 참고, 번역, 편역, 요약한 글이오니 보다 정확한 정보 습득을 원하시는 분께서는 원문을 참고하시기 바랍니다: 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 같은 공격으로부터 애플리케이션을 보호하는 방법을 살펴보겠다.

 

흥미진진 하구만~

스프링 시큐리티 맞춤확장(customization) – 파트 1. UserDetail 또는 GrantedAuthority 맞추기

참조 및 번역: http://blog.springsource.com/2009/01/02/spring-security-customization-part-1-customizing-userdetails-or-extending-grantedauthority/

이번 글은 스프링 시큐리티 맞춤확장과 관련된 실용적인 예제 중심의 여러 작은 글들의 시리즈 중 첫 번째 글이다. 이번 맞춤확장 요구 사항은 상상에서 온 것이 아니라 전부 현장에서 요구한 것이다.

다음과 같은 요구사항이 있다고 가정해보자. 역할(role) 목록이 있고 각각의 역할은 비즈니스 기능(business function) 목록을 가지고 있다.(아래를 참조하라.)

ROLE_ADMIN
  BF_QUOTE_CREATE
  BF_POLICY_CREATE
  BF_POLICY_DELETE

ROLE_AGENT
  BF_QUOTE_CREATE
  BF_POLICY_CREATE

ROLE_USER
  BF_QUOTE_CREATE

필요한 기술은 권한 결정을 ROLE과 BF 모두를 가지고 결정할 수 있어야 한다는 것이다.

예를 들어:

ROLE_ADMIN이라는 역할을 가진 사용자는 해당 역할로 보호하고 있는 모든 리소스에 접근할 수 있어야 한다.

<sec:authorize ifAllGranted=”ROLE_ADMIN”>
    <p><a href=”http://www.google.com”>Google</a>
</sec:authorize>

또는

@Secured(“ROLE_ADMIN”)
public void foo()
    . . .
}

또한 해당 사용자는 해당 역할이 가지고 있는 비즈니스 기능으로 제한하고 있는 모든 리소스에도 접근할 수 있어야 한다.

<sec:authorize ifAllGranted=”BF_POLICY_DELETE”>
    <p><a href=”http://www.google.com”>Google</a>
</sec:authorize>

또는

@Secured(“BF_POLICY_DELETE”)
public void foo()
    . . .
}

이 요구사항을 다루는 방법은 몇 가지가 있다. 그 중 하나는 RoleHierarchy를 만들고 RoleHierarchyVoter를 사용하여 역할의 계층 구조를 순회하는 것이다. 이 접근 방법의 단점은 현재 스프링 시큐리티 2.0.4 구현체의 태그 라이브러리(security: authority …)가 AccessDecisionManager를 통해서 의사 결정을 하고 있지 않으며 게다가 어떤 Voter도 제한하고 있는 HTML 엘리먼트에 대한 결정을 할 때 Role을 신경쓰지 않는다. 하지만 스프링 시큐리티의 놀라운 유연함과 맞춤확장 힘으로 인해 이 요구사항을 매우 간단하게 해결할 수 있다.
스프링 시큐리티의 가장 큰 잇점 중 하나는 어떻게 Principal(UserDetail 객체)을 생성하는지 맞춤확장 할 수 있다는 것이다. UserDetail 객체를 만들 때 GrantedAutorities 목록을 생성한다. 이 목록은 나중에 리소스를 제한하고 있는 GrantedAutority와 비교해볼 때 사용된다.
맞춤확장을 하는 또 한 가지 방법은 UserDetail 객체를 생성할 때 GrantedAuthorities 목록을 수정하는 것이다.

아래에서 제공하고 있는 예제에는 두 개의 프로퍼티 파일이 있다.(간단하게 하려고 프로퍼티 파일을 사용했지만, 여러분은 쉽게 그것을 DB나 LDAP으로 바꿀 수 있을 것이다.)

파일 하나는 users.properties로 사용자를 role에 맵핑한다.

oleg=powder,ROLE_ADMIN

다른 파일 하나는 role-to-db.properties로 각각의 role에 비즈니스 기능 목록을 맵핑한다.

ROLE_ADMIN=BF_QUOTE_CREATE,BF_POLICY_CREATE,BF_POLICY_DELETE

이제 필요한 작업은 UserDetailsService 구현체를 정의하여 위 두 개의 프로퍼티 파일을 사용하여 GrantedAUthorities 목록을 만들고 그것을 UserDetails 객체에 주입하는 것이다. 이 작업은 GrantedAuthorityImpl같은 GrantedAuthority 인터페이스 구현체를 재사용할 수 있으며 매우 간단하다. 하지만, 우리는 이때 (디버깅 또는 다른 의도로) 비즈니스 기능을 표현하는 GrantedAuthority의 상위(parent) GrantedAuthority를 추적할 수 있도록 하고 싶다.
이 두 가지 목적을 달성하기위해 GrantedAuthorityImpl을 확장하는 BusinessFunctionGrantedAuthority 클래스를 정의하고 모든 상위 GrantedAuthority 객체에 대한 목록을 담고 있게 했다.

public class BusinessFunctionGrantedAuthority extends GrantedAuthorityImpl {
    private List<GrantedAuthority> parentAuthorities;
        . . .
}

그런 다음 UserDetailsService 구현체를 만들었고 loadUserByNames(..) 메소드를 구현하여 다음 작업을 수행하도록 했다.

1. users.properties 파일 내용을 기반하여 UserAttribute 객체를 만든다. UserAttribute는 role을 나타내는 GrantedAuthority 목록을 담고 있다.
2. role-GrantedAuthorities 목록을 순회하면서 각각의 role-GrantedAuthorities 마다 BusinessFunctionGrantedAuthority를 만들고 그것을 미리 만들어둔 GrantedAuthority 목록에 추가한다.
  2-1. 각각의 BusinessFunctionGrantedAuthority에 parent GrantedAuthority를 추가한다.
3. 전체 GrantedAuthority 목록을 가지고 있는 최종 UserDetail 객체를 만든다.

그런 다음 AuthenticationProvider를 스프링 시큐리티 설정 파일에 정의한다.

사용자 삽입 이미지
노트: AuthenticationProvider에 커스텀 UserDetailService 구현체 ComplexAuthorityUserDetailsService 클래스(더 자세한 내용은 샘플 코드를 살펴보라.)를 주입했다.

리소스를 보안하고 배포한 다음 http://localhost:8080/spring-security-sample-grantedAuthority/index.jsp에 접속하라.

로그인한 뒤 여러분은 GrantedAuthorities 목록을 볼 수 있을 것이다.

사용자 삽입 이미지
여기서 여러분은 비즈니스 기능을 나타내는 GrantedAuthority가 해당 비즈니스 기능을 가지는 상위 GrantedAuthorities 목록도 보여주고 있다는 것을 확인할 수 있을 것이다.
index.jsp를 살펴보고 어떻게 security:authorize 태그를 사용ㅇ하여 role과 비즈니스 기능 목록을 사용하여 HTML 엘리먼트를 제한하는지 살펴보라
이게 끝이다. 여러분은 최소한의 맞춤확장으로 쉽게 Principal 구조를 확장하고 커스터마이징 할 수 있는지 살펴보았다. 그리고 이것을 비즈니스 코드를 더럽히지 않고 커스텀 GrantedAuthorities에 기반하여 선언적으로 보안을 적용하는지 살펴보았다.

샘플 코드는 여기서 다운로드 할 수 있다. spring-security-sample-grantedauthority

Whiteship’s Spring Security 총 정리

기본 개념

1.1. What is Acegi Security? : Acegi 정의와 개요.
2.2 Shared Components : 주요 구성 요소
2.3 Authentication (2) : 인증 절차를 도식화 했습니다.
Spring Acegi Tutorial : 가장 중요한 필터들에 대한 설명과 해당 필터들이 물고 있는 빈들을 보여줍니다.
3.2 Filters : 전체 필터들 간단 요약
4. Channel Security : https 사용에 대한 개요
Access Decision Manager : 접근 권한 관리를 하는 핵심 컴포넌트

웃긴 이야기

왜 이름이 Acegi 인가? : AbCdEfGhI, 스프링 서브 프로젝트가 되기 위한 노력.
Acegi 필터 등록할 때 발생할 수 있는 몹쓸 버그 : 필터 체인 사용하던 시절에 발생하던 웃긴 버그
Spring Security 설정 분류 및 커스터마이징 (5) : Acegi 관련 XML이 커져서 어떻게 나눌지 고민했던 시절

많이 미흡하지만 이 정도면 Spring Security 학습에 어느 정도 도움이 되실 거라고 생각합니다.
Live with Passion!!! Live with Spring!!

Spring Security 2 조만간 볼 수 있을 듯

참조 : http://blog.interface21.com/main/2007/12/06/whats-new-in-spring-security-2/

다음 주 쯤에 배포가 될 것 같습니다. 벌써부터 기다려지네요. Ben Alext가 올린 글을 보면, 설정이 상당히 간결해 진것 같은데, 너무 간결해져서 다시 공부해야 할 것 같습니다. 수 많은 필터들을 직관적인 엘리먼트와 속성이름을 사용해서 감춰주길 기대하고 있습니다.

대강 보니, 사용법은 일단 web.xml에 Filter To Chain Proxy를 등록하던 저번과 비슷한데, 대신 등록하는  클레스가 Filter To Chain Proxyr가 아니라 org.springframework.web.filter.DelegatingFilterProxy 로 바뀐 것 같습니다. 그리고 수 많은 필터를 빈으로 등록해야 했던 부분이 다음과 같은 한 덩어리로 뭉쳐진 것 같습니다.

    <http autoConfig=”true”>
        <intercept-url pattern=”/**” access=”IS_AUTHENTICATED_REMEMBER” />
    </http>

    <repository>
        <user-service hash=”md5:hex”>
            <user name=”rod” password=”a564de63c2d0da68cf47586ee05984d7″ authorities=”ROLE_SUPERVISOR,ROLE_USER” />
        </user-service>
    </repository>

TSE 2007에서 Spring Security와 관련된 세션이 세 개나 된다고 하는데, 내년에도 올해처럼 재밌는 발표들이 가득한 TSE가 되면 좋겠습니다.