CSRF란? feat. JWT 저장소에 대한 고민 1

dong5854·2022년 7월 3일
2

개발기

목록 보기
1/1

Spring Security POST 403

Spring Security를 사용하여 개발을 하던 중, POST 전송 시 403 에러가 떴다.

희한한 점은 GET 요청 시에는 정상적으로 작동한다는 점이였다.

해결을 위해 구글링한 결과 csrf를 disable 해주어야 해당 문제가 해결된다는 글을 보고 아래와 같이 disable 해주었다.

@Configuration
public class SecurityConfig{

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http
                .csrf().disable();
        return http.build();
    }
}

해당 설정을 넣어주니 아래와 같이 해결이 되었다.

참고로 구글링하다 보면 설정을 아래와 같이 설정하는 코드들이 보일텐데,

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.csrf().disable();
    }
 
    
}

하지만 spring.io에서 찾아본 글에 의하면 Spring Security 5.7.0-M2부터는 WebSecurityConfigurerAdapter가 컴포넌트 기반 security 설정을 장려하기 위해 deprecated 되었다고 한다. 따라서 해당 버전 이상부터는 위의 방법대로 설정을 해주어야 한다.


그래서 csrf는 뭘까🙄?

일단 내 POST 요청의 403 에러는 해결했다. 그런데 설정에서 내가 뭔지도 모르는걸 막 disable 하려니까 찝찝하기 그지없다. 그래서 csrf가 무엇인지 찾아봤다.

CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)


이미지 출처: https://rusyasoft.github.io/java/2019/02/15/spring-security-csrf-from-context/

CSRF란 웹 취약점 중 하나로, 이용자가 의도하지 않은 요청을 통한 공격을 의미한다. spring.io의 도큐먼트에서 CSRF가 무엇인지에 대한 예제를 제공해주는데 이를 간단하게 번역하면 아래와 같다.

당신이 사용중인 은행의 웹사이트에서 현재 로그인 중인 유저가 손을 송금할 수 있는 폼이 아와 같이 있다고 하자.

<form method="post"
	action="/transfer">
<input type="text"
	name="amount"/>
<input type="text"
	name="routingNumber"/>
<input type="text"
	name="account"/>
<input type="submit"
	value="Transfer"/>
</form>

이에 따른 HTTP 요청 헤더는 아래와 같을 것이다.

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

amount=100.00&routingNumber=1234&account=9876

이제 당신은 은행에서 볼일을 다 본 후 로그아웃을 하지 않고 나쁜 웹사이트에 접속을 하게된다. 그런데 그 나쁜 웹페이지에 아래와 같은 HTML 페이지가 있다.

<form method="post"
	action="https://bank.example.com/transfer">
<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달러가 나쁜 웹사이트 주인이 원하는 계좌로 입금된다. 이런 일이 발생하는 이유는 나쁜 웹사이트는 쿠키를 볼 수 없지만, 당신의 은행과 관련된 쿠키는 여전히 요청과 함께 전송되기 때문이다. 심지어는 win money 버튼을 누르게 할 필요도 없이 이런 프로세스가 자바스크립트를 사용해 자동화 될 수 있다.

이런 CSRF 공격에는 여러 방어 방법이 있는데 그 중 세 가지 방법이 대표적이라고 한다.

Referrer Check
백엔드에서 request의 referer를 확인하여 도메인이 일치하는지 검증하는 방법으로 일반적으로는 이 방법만으로도 대부분의 CSRF 공격을 막을 수 있다고 한다.

SameSite
SameSite는 HTTP 쿠키에서 CSRF 공격의 방지를 위해 넣을 수 잇는 설정으로 SameSite는 3가지 옵션이 있는데 이는 None ,Lax, Strict
다.
None의 경우 쿠키가 모든 상황에서 전송된다.

Lax는 쿠키가 동일한 사이트(same-site)에서의 요청이 있을 때 혹은 top-level navigation을 통해 요청이 보내질 때 쿠키를 전송한다. top-level navigationnavigating a top-level browsing context를 줄인 말로 최상위 브라우징 맥락에서 a 태그document.location 또는302 리다이텍트를 이용한 이동을 포함한다. browsing context에 관한 내용은 전에 웹 스토리지에 대해 정리하며 정리한 적이 있다. 최상위 브라우징 맥락에서의 이동만 허용이 되므로 iframe안에서 페이지를 이동하는 경우는 최상위 브라우징 맥락이 아니므로 Lax 설정된 쿠키는 전동되지 않는다.

strict는 쿠키가 동일한 사이트 내의 요청으로만 전송되도록한다.

SameSite 옵션을 사용할 때는 사용 전에 알아야할 중요한 고려사항들이 있다.

strict 속성을 사용할 때에는 상황을 고려해야 하는데, 만약 "https://social.example.com" 사이트에 로그인을 한 후 로그인 상태를 유지하고 있는 상태에서 "https://email.example.org"에서 "https://social.example.com"의 링크를 포함하는 이메일을 받게 되어 사용자가 해당 링크를 클릭하면 사용자는 로그인 상태가 유지되어있을 것이라고 생까하지만 strict 속성에 의해 쿠키가 보내지지 않아 인증받지 못해 로그인을 하지 못해 불편함을 느낄 수 있다.

또한 최신 브라우저가 아닌 경우에는 SameSite 속성을 지원하지 않을 수 있기 때문에 이는 단독 방어용보다는 이중 방어용으로 사용되는 방식이라고 한다.

CSRF Token
CSRF 공격으로부터 보호하는 다른 방법은 CSRF Token을 이용하는 방법이다. 이 방법은 세션 쿠키 외에도 각각의 HTTP요청에 CSRF Token 이라는 임의의 난수로 생성된 값이 있어야 하도록 하는 방법이다.
이 방식의 핵심은 CSRF Token은 브라우저가 HTTP Request에 자동으로 추가하는 정보가 아니여야 한다는 점이다. 예를 들잡면 실제 CSRF 토큰를 HTTP 파라미터나 헤더에서 요구하는 것은 CSRF 공격으로부터 방어가 되지만 쿠키는 브라우저에서 HTTP 요청에 자동으로 넣어주기 때문에 CSRF 토큰을 쿠키에 넣는 건은 방어법이 될 수 없다.
그렇다면 CSRF Token이 어떻게 HTTP Request에 추가되도록 하는 것일까?
이는 서버에서 웹 페이지를 발행할 때 CSRF Token 값을 넣어주고 사용자에 세션에 저장해두는 방식으로 해결한다. 위에서 보여준 은행 사이트 예시에 CSRF Token을 적용시키면 아래와 같을 것이다.

<form method="post"
	action="/transfer">
<input type="hidden"
	name="_csrf"
	value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<input type="text"
	name="amount"/>
<input type="text"
	name="routingNumber"/>
<input type="hidden"
	name="account"/>
<input type="submit"
	value="Transfer"/>
</form>

서버에서 생성해준 웹 페이지 폼에 Hidden으로 Token 값이 있는 것을 볼 수 있다. 이 폼에 의한 요청이 있을 때 서버에서는 이 Token 값이 세션에 저장된 값과 일치하는지 확인하여 해당 요청의 위조 여부를 판별한 다음에 일치 여부가 확인된 Token은 폐기되고 새로운 웹 페이지를 발행할 때마다 새로 생성한다.

그럼 이걸 왜 Disable 했나?

스프링 도큐먼트에 의하면 브라우저를 사용해 호출될 수 있는 모든 request에 대해서는 CSRF 보호를 하기를 권한다. CSRF를 Disable 해도 괜찮은 유일한 경우는 우리 서비스의 클라이언트들이 브라우저를 사용하지 않는 경우이다.

구글링을 하던 도중 JWT 토큰과 같은 stateless한 웹 어플리케이션의 경우 이를 disable 해도 괜찮다는 글을 봤는데 스프링 도큐먼트에 이에 딱 반대되는 내용이 있었다.
이 도큐먼트에 따르면 JWT 토큰을 사용할 경우에도 결국 해당 토큰을 쿠키에 넣어야 하는데 이는 위에서 세션 아이디가 쿠키에 실려 전송 되어는 것과 같은 원리로 CSRF 공격에 취약 해질 것이다.

그렇다면 JWT 토큰을 local storage에 저장한다면?

그렇다면 JWT 토큰을 Local Storage에 저장한다면, Local Storage는 동일 출처 정책(same-origin policy)을 갖기 때문에 위의 예시와 같은 방법으로 공격이 불가능하니 문제가 해결되는것 아닐까? 라고 생각을 했다. 그래서 리서치를 해본결과 해당 방식을 사용하면 XSS라는 공격에 취약해진다고 한다.

그럼 어쩌라고🤦‍♂️?

JWT를 쿠키에 저장하자니 CSRF에 취약하고 로컬 스토리지에 저장하자니 XSS라는 공격에 취약하다고 한다. 그러면 어쩌라는 걸까..? 라고 생각해서 검색해본 결과 정답은 없다는것 같고 개발자들마다 각자의 견해가 다르게 있는 것 같다.

우리는 어떻게 해야할까?

우리 프로젝트는 JWT 토큰을 사용한 인증을 할 예정이므로 이에 대한 고민이 필요하다고 생각한다. 물론 토이프로젝트에서 보안이 중요하지는 않겠지만 이런 고민을 해보는 것이 스스로의 성장에 도움이 될 것이라 생각한다. 그렇게 때문에 다음 포스팅에서 XSS 공격에 대해 알아보고 JWT 토큰의 저장 장소에 대한 고민을 더욱 깊게 해볼 생각이다. 그래서 이 포스팅의 제목을 "CSRF란? feat. JWT 저장소에 대한 고민" 이 아니라 뒤에 1을 붙여 "CSRF란? feat. JWT 저장소에 대한 고민 1" 로 지었다 다음 포스팅제목은 "XSS란? feat. JWT 저장소에 대한 고민 2" 정도가 될 것 같다.


그냥 단순히 403에러 하나 해결하려고 한 구글링이 JWT 토큰 저장소에 대한 고민까지 올줄은 몰랐다... 항상 느끼는 거지만 개발 공부는 항상 꼬리에 꼬리를 무는 것 같다. 다음 포스팅도 빠른 시일 내에 할 수 있기를 바라면서...

끝.

참고자료
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
https://rusyasoft.github.io/java/2019/02/15/spring-security-csrf-from-context/
https://docs.spring.io/spring-security/reference/features/exploits/csrf.html
https://docs.microsoft.com/ko-kr/azure/active-directory/develop/howto-handle-samesite-cookie-changes-chrome-browser?tabs=dotnet
https://minkukjo.github.io/cs/2020/08/15/Security-1/
https://cjw-awdsd.tistory.com/48
https://velog.io/@0307kwon/JWT%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-localStorage-vs-cookie

profile
https://github.com/dong5854?tab=repositories

1개의 댓글

comment-user-thumbnail
2023년 10월 6일

cookie 방식을 사용하면서 httpOnly, Secure, same-site:strict 3개 옵션을 취하는게 가장 안전하지 않을까요?

  • httpOnly: XSS 대응
  • Secure: https (TLS/SSL) 통신을 통해서만 쿠키 전송
  • same-site:strict 는 CSRF 대응
답글 달기