슬기로운 개발생활

Mustache CSRF 적용 및 문제해결

by coco3o
반응형

시작하기 앞서, CSRF에 대해 간단히 알아보고 가도록 하자

csrf 란?
- 사이트 간 요청 위조(Cross-site request forgery)의 약자로 웹 애플리케이션 취약점 중 하나로 사용자가 자신의 의지와 무관하게 공격자가 의도한 행동을 하여 특정 웹페이지를 보안에 취약하게 한다거나 수정, 삭제 등의 작업을 하게 만드는 공격 방법을 의미한다. - 나무위키 -

공격과정
- 2008년도에 있었던 옥션 해킹 사고도 CSRF 공격을 했다고 한다. (해커가 옥션 운영자에게 CSRF 코드가 포함된 이메일을 보내서 관리자 권한을 얻어냈다)
...
<img src="http://auction.com/changeUserAcoount?id=admin&password=admin" width="0" height="0">
...

1. 옥션 관리자 중 한 명이 권한을 가진채 회사 내에서 작업을 하던 중 메일을 조회한다. (로그인이 되어있으니 관리자로서의 유효한 쿠키를 가지고 있음)
2. 해커는 위와 같이 태그가 들어간 코드가 담긴 이메일을 보낸다.

3. 관리자가 이메일을 열어볼 때, 이미지 파일을 받아오기 위해 위 URL이 열린다.
4. 해커가 의도한 대로 관리자의 계정이 id와 pw가 admin으로 변경된다.
참고


Spring Security를 적용하면, 기본적으로 csrf라는 기능이 활성화된다.

csrf 공격을 막기 위해 csrf 토큰을 사용하는 것이다.

Spring Security에서는 form 전송 시 csrf 토큰을 함께 전송해주어야 한다. csrf 토큰 없이 요청하면 해당 요청을 막아버린다.

필자는 csrf를 비활성화한 후 회원가입 / 로그인 기능을 먼저 구현했다.

이제 csrf를 활성화 해 csrf 토큰을 전송하도록 바꾸기 위해 알아보았더니,

타임리프나 다른 템플릿 엔진은 csrf 토큰을 기본적으로 제공해주는 반면, 머스테치는 제공해주지 않았다.


검색을 통해 알아보니 인터셉터를 구현해서 모델 어트리뷰트로 넣어주는 방법이 있었는데, 우선 이 방법은 실패했다.

그러다 스택오버플로우에서 방법을 찾아냈다.
참고 : https://stackoverflow.com/questions/26397168/how-to-use-spring-security-with-mustache

application.properties에서 mustache 설정을 추가해준다.

# MUSTACHE
spring.mustache.expose-request-attributes=true

그리고 form 안에 hidden으로 csrf 값을 넣어줬더니 csrf로 인한 필터는 통과할 수 있게 되었다.

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

이제 됐다 싶었는데, 곧바로 두 가지 문제에 직면했다.

1. 로그인 후 글 작성을 하면 아래와 같이 403 Forbidden 에러가 나온다.

곰곰이 생각해보니 게시글 관련은 REST API를 적용하고 있어 csrf 토큰 사용을 못하는 거였다.

이렇게 특정 URL 외부 프로그램 등에서 POST방식으로 서버에 접근하면 403 에러가 발생한다.

이런 경우 해당하는 특정 URL만 csrf 적용받지 않도록 예외 처리해주어야 한다.

그래서 다음과 같이 configure(HttpSecurity http) 메서드를 수정해주었다.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().ignoringAntMatchers("/api/**")
                .and()
                .authorizeRequests()
                .antMatchers("/", "/auth/**", "/posts/read/**", "/posts/search/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/auth/login")
                .loginProcessingUrl("/loginProc")
                .defaultSuccessUrl("/");
    }
  • .csrf().ignoringAntMatchers("/api/**")
    • api로 시작하는 경로는 csrf 보호대상에서 제외시킨다.

이렇게 해주니 글 작성, 수정, 삭제까지 잘 되었다.


2. 로그아웃 시 404 화면이 나온다.

CSRF가 활성화된 경우 로그아웃 요청도 기본적으로 POST여야 한다. 즉, POST "/logout"이 필요하다.

※ POST가 default인 이유? 사용자를 어플리케이션에서 강제로 로그아웃시키는 CSRF 공격을 방지하기 위함.

필자는 a 태그를 사용해서 로그아웃을 요청했기 때문에 404 화면을 봤던 것이었다.

UserController

다음과 같이 UserController에 로그아웃 메소드를 추가했다.

    @GetMapping("/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) throws Exception {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null) {
            new SecurityContextLogoutHandler().logout(request, response, auth);
        }
        return "redirect:/";
    }

코드를 보면 POST 가 아닌 GET으로 작성되어 있는데, 필자는 form이 아닌 a태그를 사용해 로그아웃 요청을 하고 싶기 때문에
GET 방식으로 로그아웃 하도록 우회해줬다.

컨트롤러에서 매핑을 받고, 이걸 로그아웃 기능으로 돌려버리는 것이다.

이렇게 GET을 통해 로그아웃 처리를 하는 경우엔 LogoutSuccessHandler는 무시한다는 사실을 알아야 한다.

반응형

블로그의 정보

슬기로운 개발생활

coco3o

활동하기