▸Spring Security

Spring Security_CSRF Token의 개념과 사용 방법

코데방 2020. 4. 22.
728x90
- Develop OS : Windows10 Ent, 64bit
- WEB/WAS Server : Tomcat v9.0
- DBMS : MySQL 5.7.29 for Linux (Docker)
- Language : JAVA 1.8 (JDK 1.8)
- Framwork : Spring 3.2.9 Release
- Build Tool : Maven 3.6.3
- ORM : Mybatis 3.2.8

 

[ CSRF(Cross Site Request Forgery) 공격 ]

  • 사용자 의지와 무관하게 공격자의 의도대로 서버에 특정 요청을 하도록 함

지금까지 작성한 컨트롤러 코드를 보면 특정 URL에 대한 요청을 처리기 메소드가 받아서 처리 후 뷰 페이지로 돌려주는 형태입니다. 물론 RESTful API라면 뷰 대신 데이터를 돌려줍니다.

 

만약 이 상태에서 누군가 서비스를 사용한다고 가정해보겠습니다. 사용자는 정상적으로 로그인을 하고 권한을 획득합니다. 이 때 공격자가 몰래 자신이 만든 페이지로 피싱 사이트를 하나 만들어 사용자가 접속하게 유도합니다. 화면이 완전 동일하기 때문에 사용자는 의심 없이 사이트에 정보를 입력하고 서버에 요청을 보냅니다. 하지만 실제 서버에 요청되는 내용은 공격자가 심어둔 내용입니다. 이렇게 사용자의 권한을 이용해 서버에 변조된 요청을 보내는 공격 방식을 CSRF라고 합니다.

 

 

간단한 예를 들어 맛스타그램에 사진을 하나 업로드하기 위해 로그인을 한 상태라고 해보겠습니다. 사진을 올리기 전 공격자는 글쓰기 페이지를 본인이 만든 페이지로 바꿔치기 합니다. 접속 URL이 변경되겠지만 아주 미묘한 차이로 만들어졌다면 인지하기가 어렵습니다. matstagram.com을 matstaqram.com 으로 바꾸는거죠. Host 파일이나 DNS 정보만 조금 변경해줘도 크게 어렵지 않은 작업입니다.

 

이 페이지에는 Hidden 타입으로 공격자가 원하는 실제 데이터가 들어있고 사용자가 전송 버튼을 누르면 서버에 요청과 함께 전송됩니다. 사용자가 제목으로 "안녕하세요"를 입력했지만 이 정보는 서버에 전송되지 않고 공격자가 입력해둔 "돈 빌려드립니다~!"가 입력되는 것이죠. 이렇게 내 계정에 공격자가 만든 게시물이 나도 모르게 올라갈 수 있다는 의미입니다.

 

 

이렇게 실제 서버에서 받아온 뷰 페이지가 아닌 위조된 페이지에서 서버에 요청을 보내는 행위를 걸러내기 위한 방법이 다양하게 존재합니다. 그 중 한 방법이 CSRF Token이라는 것을 사용해, 서버에 요청을 올린 페이지가 실제 서버에서 발행한 뷰 페이지가 맞는지 확인하는 것입니다.

 

 


 

 

[ CSRF Token ]

  • 서버에 들어온 요청이 실제 서버에서 허용한 요청이 맞는지 확인하기 위한 토큰

서버에서는 뷰 페이지를 발행할 때 랜덤으로 생성된 Token을 같이 준 뒤 사용자 세션에 저장해둡니다. 그리고 사용자가 서버에 작업을 요청할 때 페이지에 Hidden으로 숨어있는 Token 값이 같이 서버로 전송되는데, 서버에서는 이 Token 값이 세션에 저장된 값과 일치하는지 확인하여 해당 요청이 위조된게 아니라는 것을 확인합니다. 일치 여부를 확인한 Token은 바로 폐기하고 새로운 뷰 페이지를 발행할 때마다 새로 생성합니다.

 

 

물론 정상 발행된 페이지 안의 Token값을 확인한 뒤 새로고침 하기 전에 이를 뺏어오는 방법도 있겠지만 훨씬 고도화된 작업이 필요하게 됩니다. 단순히 위조된 페이지만 사용자에게 보여주는 것으로는 CSRF 공격이 불가하다는 의미이고 그만큼 보안 수준이 높아짐을 의미합니다.

 

 


 

 

[ Spring Security를 이용한 CSRF Token 적용 ]

 

CSRF방어 기능은 Spring Security 3.2.0 이후부터 지원됩니다. 시큐리티는 스프링의 버전과 동일해야하기 때문에 스프링 버전을 높여주면 됩니다. 저는 초기 셋팅대로 3.1.1 Release 버전을 사용하고 있어서 이번 기회에 업그레이드를 해줬습니다. pom.xml 파일에서 버전정보만 변경해주면 됩니다. 호환성에 관한 문제는 차차 나올때마다 해결하면 될 것 같습니다. 아직까진 문제가 없네요.

 

	<properties>
		<java-version>1.8</java-version>
		<org.springframework-version>3.2.9.RELEASE</org.springframework-version>
		<org.aspectj-version>1.6.10</org.aspectj-version>
		<org.slf4j-version>1.6.6</org.slf4j-version>
	</properties>

 

 

 

1. 컨텍스트 설정 파일에서 CSRF 적용

 

간단히 <http> 태그 안에 아래 설정만 추가해주면 됩니다. 한줄로 적용이 끝이고, 모든 페이지에 CSRF Token 값이 생성되어 추가됩니다. 토큰 값은 1회용이라 유출되더라도 악용될 가능성이 높지는 않습니다.

		<!-- CSRF -->
		<s:csrf/>	
	</s:http>

 

 

 

2. POST 방식의 데이터 전송에 CSRF Token 값 추가

 

CSRF 설정을 적용한 뒤부터는 모든 POST 방식의 데이터 전송에 토큰 값이 있어야 합니다. GET 방식에는 적용되지 않습니다. 만약 POST 전송 데이터에 유효한 Token값이 없다면 권한이 없는 페이지에 접근할 때와 동일하게 에러 페이지로 이동됩니다. 

 

토큰값은 세션에 지정된 이름으로 저장돼있기 때문에 아래와 같이 FORM 태그 안에 삽입해주면 됩니다. 토큰의 이름과 값의 파라미터 이름입니다. Spring Security를 사용하지 않고 그냥 코드에서 Session Attribute에 랜덤 값을 생성해 뷰에 전달해주고 받을 때 검증하는 로직을 짜도 동일하게 작동합니다. 이 과정을 MVC 앞단에서 처리해줍니다.

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />

 

 

위 태그는 외우기도 힘들고 매우 번거롭기 때문에 아래의 시큐리티 태그를 사용하면 편하게 삽입할 수 있습니다. 마찬가지로 Form 태그 안에 넣어주면 됩니다.

<s:csrfInput />

 

 

3. 로그아웃 링크를 POST 방식으로 수정

 

Spring Security를 이용해 로그아웃을 처리하고 있다면 POST 방식으로 전송하도록 되어 있기 때문에 전송 방식을 바꿔줘야 합니다. 대부분 로그아웃은 단순 링크 방식의 GET 방식으로 되어 있을 것 같습니다. 

 

위의 내용처럼 간단히 Form 태그를 이용해 로그아웃 버튼을 만들어 줄 수 있습니다. POST 방식에는 CSRF 토큰도 항상 같이 넣어줘야 합니다. 

<form action="/logoutAsk" method="post">
	<s:csrfInput/>
	<input type="submit" value="로그아웃">
</form>

 

 

 

저 같이 <a> 태그를 사용해 링크를 걸고 있었다면 Form 태그로는 디자인을 맞추기가 어려울 수 있습니다. 이 경우 <a> 태그의 링크를 POST 방식으로 전송하기 위해서는 자바스크립트를 사용해야 합니다.

 

먼저 Form 태그를 작성합니다. 자바스크립트에서 이 Form을 찾아서 Submit을 해줄 수 있도록 name을 추가로 붙여줍니다.

<form name="logoutAskOne" action="/logoutAsk" method="post">
	<s:csrfInput />
</form>

 

그리고 사용중인 자바스크립트(.js) 파일 아무곳에나 아래와 같이 코드를 작성해줍니다. 가장 기초적인 사용법입니다.

// 버튼 클릭 지정된 Form 실행
function btnClick(formName) {
	formName.submit();
};

 

만약 자바스크립트 파일이 따로 없다면 아래와 같이 작성해서 </body> 태그 위에 넣어줘도 됩니다.

<script type="text/javascript">
	function btnClick(formName) {
		formName.submit();
	};
</script>

 

 

이제 실제 <a> 태그가 있는 곳으로 가서 링크를 눌렀을 때(onclick) 자바스크립트의 코드가 실행될 수 있도록 해줍니다. 클릭하면 자바스크립트 메소드가 Form 태그의 이름을 찾아서 대신 Submit을 해줍니다.

<a href="#" onclick="javascript:btnClick(logoutAskOne);">로그아웃</a>

 

 


 

 

CSRF 토큰 필터는 GET방식의 데이터 전송에는 관여하지 않습니다. 만약 GET방식까지 막아버리면 다른 사이트에서 링크를 타고 들어오는 요청이나 RESTful API 등을 처리할 수가 없게 됩니다. 저 같은 경우는 자동 로그인 요청에 대한 인증 메일에서 모든기기 로그아웃을 할 수 있도록 했었는데 외부에서는 토큰 정보가 없으니 작동하지 않게 됐네요. 로직 자체가 어렵지는 않으니 직접 CSRF 필터를 하나 만들어볼까도 했지만 일단은 그냥 사용해보겠습니다. ㅎㅎ

728x90

댓글

💲 추천 글