개발 기록

JQuery Ajax + Thymeleaf CSRF 처리 본문

spring/Spring Security

JQuery Ajax + Thymeleaf CSRF 처리

청군로 2022. 5. 31. 09:11

프로젝트 보안 취약점 점검 중 CSRF 토큰이 없다는 경고를 받았다.

 

 

CSRF 란
특정 사이트에서 유저가 보내는 요청을 조작하는 공격이다.
CSRF Token은 클라이언트가 유효함을 알릴 수 있는 일종의 보안카드이다.

1. 서버는 클라이언트에게 CSRF Token 발급
2. 클라이언트는 서버에 Request 시, 발급 받은 Token 첨부
3. 서버가 Token을 검증하고 정상적으로 Request를 처리

 

Spring Security 에서는 @EnableWebSecurity 어노테이션을 지정할 경우 자동으로 CSRF 방어 기능을 지원하고 있다.

따라서 자동으로 활성화 되어야 하는 CSRF 방어가 안되고 있던 것. (security 3.2.0 부터 csrf 방어 지원)

 

Security Config 파일로 이동해보니 다음과 같은 코드가 있었다.

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.csrf().disable();
    
    // ... 나머지 코드
}

 

csrf 기능을 비활성화하는 코드이다. 아무튼 보안 취약점에 걸렸으니 csrf 보안이 동작하게 코드를 수정했다.

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
	
    // ...나머지 코드
}

다음과 같이 설정을 수정하면 CSRF 방어가 활성화 된다. 내가 사용하고 있는 타임리프의 경우 form 태그를 사용할 시

자동으로 csrf Token을 밀어넣어준다고 한다.

 

CSRF 방어 기능을 활성화 했으니 끝인 줄 알았으나 몇 가지 문제가 있었다.

  1. 로그아웃
  2. ajax를 사용해서 post 요청을 할 때

로그아웃

코드 변경 후 개발을 이어서 하는데, 로그아웃 기능이 제대로 처리되지 않고 있었다. 기존에는 로그아웃 버튼을 누르면 /logout 으로 redirect 시켜버렸는데 구글링을 해보니 csrf 방어 기능 활성화 시, 로그아웃은 post로 처리해야 됐다.

> 수정 전 로그아웃

<a th:href=@{/logout}>로그아웃</a>

 

> 수정 후 로그아웃

<form id="logoutForm" th:method="post" th:action="@{/logout}">
	<a id="logoutBtn" onclick="document.getElementById('logoutForm').submit();">로그아웃</a>
</form>

 

ajax를 사용해서 post 요청

간혹 form을 submit 하기 전 유효성 검사라든지, 비동기 처리 등 ajax를 사용해서 post 전송을 할 때가 있다.

form 태그를 사용할 때는 타임리프가 csrf를 자동으로 처리해줬는데, 직접 처리할 경우 다음처럼 csrf 토큰을 구할 수 있다.

  • meta 태그에 csrf Token 랜더링
  • input 태그에 hidden 타입으로 csrf Token 랜더링
  • script 에서 타임리프 문법으로 변수에 담아서 사용

나는 meta 태그에 랜더링해서 가져다 쓰는 방법을 선택했다.

<meta name="_csrf" th:content="${_csrf.token}"/>
function test() {
	const token = $("meta[name='_csrf']").attr("content");

	$.ajax({
    	url: "/test",
        type: "POST",
        data: {
            name: "홍길동",
            age: 30,
            _csrf: token
        }
    })
}

data에 _csrf를 추가하지 않으면 403 Forbidden 에러가 발생한다. (서버가 클라이언트의 접근을 거부할 때 발생)

 

마지막으로 꽤나 고생했던 경우가 있다. ajax로  post 전송을 할 때 data를 Json 문자열로 변환한 경우이다.

서버에서 데이터를 Request Body(객체)로 받는 경우 ContentType을 application/json 으로 하는데, 이 경우 csrf 처리 x

 

csrf 토큰 처리가 제대로 안되는 코드

function test() {
    const token = $("meta[name='_csrf']").attr("content");
    const params = {
    	name: "홍길동",
        age: 30,
        _csrf: token
    }

    $.ajax({
    	url: "/test",
        type: "POST",
        contentType: "application/json; charset=utf=8",
        data: JSON.stringify(params)
    })
}

 

이때는 csrf Token 정보를 data 쪽이 아닌 header에 넣어줘야 한다. 초반에 시도했던 코드

function test() {
    const token = $("meta[name='_csrf']").attr("content");
    const params = {
    	name: "홍길동",
        age: 30
    }

    $.ajax({
    	url: "/test",
        type: "POST",
        contentType: "application/json; charset=utf=8",
        data: JSON.stringify(params),
        haders: {
            X-CSRF-TOKEN: token
        }
    })
}

이 역시도 안돼서 삽질에 삽질을 하다 다음 코드로 해결했다.

 

성공한 코드

<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
function test() {
    const token = $("meta[name='_csrf']").attr("content");
    const header = $("meta[name='_csrf_header']").attr("content");
    const params = {
    	name: "홍길동",
        age: 30
    }

    $.ajax({
    	url: "/test",
        type: "POST",
        contentType: "application/json; charset=utf=8",
        data: JSON.stringify(params),
        beforeSend: function(xhr) {
        	xhr.setRequestHeader(header, token);
        }
    })
}

_csrf headerName을 meta 태그에 추가하고 beforeSend로 설정을 해주니 비로소 요청이 서버까지 잘 전달 됐다.

 


REFERENCE

Spring Boot CSRF AJAX 전송 방법

Comments