본문 바로가기
프로젝트/Recipository

[Dev] 22.10.28. Post Fetch Request and CSRF Token with Spring Security

by 규글 2022. 10. 28.

 가장 먼저 마주한 것은 Spring Security를 사용하면서 Fetch를 통한 Post 방식 data를 server로 전달하는 것이다. 처음 페이지와 기능을 구현했을 때는 Spring Security를 들을 생각을 하지 않아서 단순히 검색을 통해서 여러 Filter 들을 disabled 시켜서 작업했었다. 하지만 지금은 방식을 달리해야 했고, 가장 먼저 회원 가입 단계부터 수정해야 했다.

 

 현재는 javascript를 통해서 validation check를 한다. (당장은 이렇지만 회원 가입과 로그인 기능에 대한 작업을 마무리하고서 다시 valiation을 back 단에서 하는 방식으로 수정해볼 생각이다.) 그리고 입력한 정보에 대한 validation 과정에서 Fetch를 통해 Post 방식으로 ajax request를 보낸다. 문제는 이곳에서 발생했다. 기존에는 csrf가 disabled 되어서 data를 server로 보내는 것에 문제가 없었지만, 해당 내용을 지우고서 작업하니 browser 다음과 같은 메시지를 확인하게 되었다.

 

 위 오류는 입력한 아이디에 대한 중복 확인을 위한 ajax request인데, 기존에는 정상적으로 동작하던 것이 이번에는 오류를 발생시키고 있었다. 해당 오류는 csrf를 disabled 시키면 등장하지 않는 내용이다. 따라서 CsrfFilter를 통과하도록 해야 했다. 작업에 앞서서 해당 친구가 어떤 것인지 알아야 했다.

 

CSRF

 CSRF 라는 것은 Cross-Site Request Forgery 의 줄임말로 Forgery는 '위조죄(fake), (서류나 지폐 등의) 위조된 물건' 이라는 의미를 가지고 있어서, '사이트 간 요청 위조' 라는 의미이다. 사용자가 자신의 의지와 무관하게 공격자가 의도한 행동을 해서 특정 웹페이지를 보안에 취약하게 한다거나 수정, 삭제 등의 작업을 하게 만드는 공격 방법이다.[각주:1]

 예를 들어 나쁜 의도를 가진 사람이 특정 사이트에 대한 공격을 하려고 한다고 가정해보자. 이때 해당 사이트에 로그인 한 선량한 사람이 있는데, 이 사람이 나쁜 사람이 보낸 수상한 요청 담긴 img 를 보내고 선량한 사람이 이를 클릭했을 때 사이트에 대한 공격이 이루어지는 과정을 거친다. 이미 로그인 한 사용자로부터의 요청이므로 사이트는 믿을 수 있는 사용자로부터의 요청이라고 판단하는 것이다. 간단히는 로그아웃을 하게끔 할 수도 있고, 심각하게는 개인정보를 변경한다든지 할 수도 있다.

 

 POST 나 PUT 과 같이 server의 resource를 변경하는 요청의 경우 이를 막기 위해 Security Filter 중에 CsrfFilter에서는 server에서 내보낸 리소스에서 올라온 요청인지를 구분한다. 이 Filter에서는 POST와 PUT과 같이 server resource를 변경하는 요청인 경우, 내려보낸 resource에서 오는 request인지를 확인하는 것이다. 그래서 매 요청마다 csrf token 정보를 포함해서 전송하고, 그것을 server의 것과 비교해서 요청의 접근을 허용하는 방식을 취하는 것 같다.

 

 

CSRF 공격에 대한 대응 방법

 크게 다음의 세 가지를 언급하는 곳을 발견할 수 있었다.[각주:2] [각주:3]

  1. CAPTCHA
  2. Referer 검증
  3. CSRF token

1. CAPTCHA

 검색해보니 디스코드나 기타 회원 가입 시 볼 수 있는 위와 같이 사람인지 아닌지 체크하는 것이나, 여러 그림을 grid로 주고 특정 사물을 체크하는 것을 크게 CAPTCHA 라고 한다. 이 친구로도 방어가 가능하다고는 한다.

 

2. Referer 검증

public class ReferrerCheckInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String referer = request.getHeader("Referer");
        String host = request.getHeader("host");
        if (referer == null || !referer.contains(host)) {
            response.sendRedirect("/");
            return false;
        }
        return true;
    }
}

 Request header의 referer 와 host 의 값을 비교한다. 이때 referer에 host의 값이 있는지 확인하는 방식으로 검증을 하는 것 같다.[각주:4] 해당 내용은 여기에서 다룰 내용은 아닌 것 같아서 언급만 하고 넘어간다.

 

3. CSRF token

  Spring docs에 소개된 내용을 가지고 왔다. 두 번째 footnote는 번역되어 있다.[각주:5] [각주:6]

 

 Docs에는 CSRF 공격에 방어하기 위해서 HTTP request에 CSRF token을 반드시 포함시켜야한다고 적혀있다. 이때 form 안쪽에 parameter로 전달해야 하거나 HTTP header에 포함시키는 등, browser가 자동으로 해주지 않는 영역에 대해 token을 포함시킬 수 있도록 해야 한다.

 

 Spring docs에서는 다음의 두 가지 방식을 소개하고 있다.

 

  1. Form URL Encoded (html form tag 안쪽에 input type hidden 으로 csrf token 값 넣기)
  2. Ajax and JSON Request (head tag 안쪽의 meta tag에 csrf token 값 받아서 사용하기)

 

1. input type hidden

<form>
    <input type="hidden"
        name="_csrf"
        value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
    ...
</form>

  HTML form을 통해 POST request를 위해서는 input type hidden으로 csrf token 정보를 넣어주어야 한다. 만약 Spring Security를 사용한다면 CsrfRequestDataValueProcessor 를 통해 Spring의 RequestDataValueProcessor와의 통합을 지원하는데, 그러면 이 RequestDataValueProcessor 와 통합될 수 있는 view technology를 포함해서 Spring의 form tag library나 thymeleaf를 사용하는 경우에 form의 data를 POST, PUT, DELETE 등의 server 의 resource를 변경할 수 있어서 안전하지 않은 HTTP request 방식을 사용한다면 자동으로 CSRF token을 form에 넣어준다.

 

 

 만약 자동으로 넣어주지 않는다고 하더라고 HttpServletRequest에 '_csrf' 라는 이름으로 token 값이 전달되니까 받아서 사용하는 방식을 취해도 된다.

 

 

2. meta tag

<html>
<head>
    <meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
    <meta name="_csrf_header" content="X-CSRF-TOKEN"/>
    <!-- ... -->
</head>

 JSON을 사용하는 경우 HTTP parameter로 CSRF token을 함께 submit 할 수가 없는데, 이때는 HTTP header에 CSRF token을 담아서 전달한다.

 먼저 CSRF token을 Cookie에 담도록 할 수 있는데, cookie에 담아 보내면 Angular JS의 경우 자동으로 HTTP header에 담는다고 한다. 하지만 필자는 그렇지 않기때문에 HTML head tag 안쪽에 meta tag로 받아서 사용하는 방법을 택했다. 이렇게 meta tag로 받으면 javascript를 통해서 해당 값을 읽어서 사용할 수 있게 된다.

 

        var token = document.querySelector("meta[name=_csrf]").content;
        var header = document.querySelector("meta[name=_csrf_header]").content;
        
        var promise = fetch(url, {
                method: "POST",
                headers: {
                    "header": header,
                    "X-Requested-With": "XMLHttpRequest",
                    "Content-Type": "application/json",
                    "X-CSRF-Token": token
                },
                body: JSON.stringify(object)
        });

 이때 fetch 안에서 headers에 위와 같은 방식으로 header 정보를 넣어준다.[각주:7] 위 코드에서 header 정보 중에 header와 X-Requested-With 이 없어도 동작 자체는 정상적으로 되며, header의 경우 작성하지 않아도 이미 해당 값이 들어가 있는 것을 확인했다.

X-Requested-With 은 ajax request임을 표기하는 용도로 사용되는데, 표준은 아니라고 말한다.[각주:8] 해당 내용이 default로 작성되는 것인지는 알 수 없다.

 

 

의문점

 여기서 생기는 의문이 있다. 그렇다면 이미 로그인 한 선량한 사람의 경우 browser에 csrf token 정보가 이미 있을텐데, 그렇게 되면 공격 요청이 담긴 무언가를 클릭하면 정상적으로 요청되는 것은 동일하지 않을까 하는 점이다. 예를 들어 선량한 이용자의 정보를 변경하는 POST 요청을 악의적인 사람으로 인해 하게 되었다고 생각해보면, 단순히 form을 제출하는 사항만 있어서 자동으로 input hidden이  들어가는 것이 불가능한 경우 browser에는 이미 meta tag로 token 정보가 있을 것이다. 그렇다면 해당 내용을 악의적인 사람의 form에 script로 추가할 수 있는 것 아닐까?

 

 

XSS 와 CSRF 와의 차이

 XSSCross-Site Scripting 의 줄임말로, Cross 의 모양인 X를 사용해 XSS 라고 하는 것 같다. 글자 그대로 공격하고자 하는 사이트에 script를 넣어 공격하는 방식이라고 한다. 따로 선량한 사용자의 로그인을 필요로 한다든지의 조건이 필요가 없다. 다음의 방식을 예로 들 수 있다.

  • 보안에 취약하게 만든 게시판의 경우 게시글에 script를 넣어 저장하면, 후에 해당 게시글을 열었을 때 script가 동작함.
  • 쿠키에 중요 정보가 포함되어 있는데 대응이 준비되지 않은 경우 script를 통해 탈취하는 방법도 가능함.

 

 CSRF는 선량한 사용자가 로그인을 한 상태로 의도하지 않은 request를 보내도록 하는 방식이라고 한다. 다음을 예로 들 수 있다.

  • 이미지나 링크를 클릭하게 하거나, 화면을 통해서 원치 않는 request를 보냄. 해당 이미지나 링크 등에는 form 과 그에 대한 script가 들어있기도 함.
  • 아주 유사한 사이트를 만들어서 해당 form에 유저 스스로 data를 입력하도록 하고 action은 server로 하도록 함. 굳이 직접 입력하는 방식이 아니어도 해당 사이트를 방문하는 것으로 선량한 사용자가 의도하지 않는 request를 보내는 것도 가능함.

 

 CSRF 에 두 번째로 작성한 내용을 기반으로 생각했을 때, header의 referer와 host를 비교해서 검증하는 방향인 것 같다. 그리고 다른 사이트에서의 form에는 당연히 input type hidden 으로 csrf token 값이 없으므로 당연히 server 의 Filter를 통과할 수 없을 것이다. Token을 들고 다른 사이트에서의 script로 값을 넣어준다고 해도 referer 와의 비교를 통해 방어가 가능할 것 같다.

 마지막으로 둘을 섞어서 사용하는 경우에는 html tag를 tag로서 동작하지 못하도록 한다면 browser에 출력된 token의 내용을 사용하는 것도 불가능해질 것이다.

 

 기타 조금 더 살펴볼 수 있는 몇 사이트를 적어둔다.[각주:9] [각주:10]

 

 

 최근에 지인을 만났을 때, 게시글에 html tag가 정상적으로 동작할 수 없도록 막아둔 내용을 보여주었다. 당시에는 납득할 수 없었으나 이번 내용을 이리저리 찾아보면서 해당 내용이 왜 필요한지 알게 되었다. 완벽하게 이해했다고 생각할 수는 없으나, 납득이 가는 정도로 받아들인 것 같다.

 

Reference

댓글