[Spring] CSRF

유시준·2022년 1월 7일
0
post-custom-banner

CSRF란?

Cross Site Request Forgery로 대충 직역하면 교차 사이트 요청 변조이다.
대충 사이트에서 요청을 변조한다는 의미로서 인증된 유저의 계정을 이용해 악의적인 변경요청을 만들어 이익을 취하는 공격방법이다.

CSRF의 공격과정

쿠키와 세션

서버는 사용자가 로그인을 하면 사용자의 정보를 세션에 저장하고 클라이언트는 세션ID를 쿠키에 저장한다. 서버는 쿠키에 담긴 세션ID를 통해 사용자의 인증여부를 파악한다.

전제조건

  • 사용자가 서버로 부터 인증을 받은 상태여야 한다.
  • 쿠키에서 서버의 세션정보를 획득할 수 있어야 한다.
  • Request방식을 파악하고 있어야함.

과정

  1. 사용자가 로그인을 함
  2. sessionID가 사용자의 쿠키에 저장됨
  3. 사용자가 악성 스크립트 페이지를 누르도록 유도
    • 사이트게시판 혹은 메일등으로 악성스크립트를 전달 혹은 링크를 전달하여 클릭 유도함
  4. 사용자가 악성스크립트가 작성된 페이지에 접근시 쿠키에 저장된 sessionID로 서버에 요청함
  5. 서버는 sessionID를 통해 정상적인 사용자로 판단 후 요청 처리

방어

  • stateless한 restAPI에서는 쿠키나 세션을 사용하지 않기때문에 csrf보호가 따로 필요가 없기 때문에 disable로 처리한다.

  • Referrer 검증

    • 서버에서 사용자의 요청에 Referrer 정보를 확인
    • 호스트(host)와 Referrer 값이 일치하기 때문에 비교
    • 대부분의 CSRF 공격의 경우 Referrer 값에 대한 검증만으로 방어가 가능함
  • Spring Security CSRF Token 사용

    • 임의의 토큰을 발급한 후 자원의 변경에 대한 요청(post,put,delete)일 경우 Token값을 확인하여 클라이언트의 요청이 정상적인 경로인지 확인함
    • Token이 일치하지 않을 경우 403상태코드를 리턴

스프링 시큐리티

동작 방식

  • 스프링 시큐리티에서는 @EnableWebSecurity 어노테이션이 기본적으로 CSRF 공격을 방지하는 기능을 지원함

  • 스프링 시큐리티에서 CsrfFilter가 구현되어 있다.
    CsrfFilter 클래스

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
                // 1. 저장되어 있는 csrf token확인
		request.setAttribute(HttpServletResponse.class.getName(), response);
		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		boolean missingToken = (csrfToken == null);
		if (missingToken) {
         	        // 2. 서버에서 csrf token 생성
			csrfToken = this.tokenRepository.generateToken(request);
			this.tokenRepository.saveToken(csrfToken, request, response);
		}
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);
        // 3. csrf filter 사용 유무 확인
		if (!this.requireCsrfProtectionMatcher.matches(request)) {
			if (this.logger.isTraceEnabled()) {
				this.logger.trace("Did not protect against CSRF since request did not match "
						+ this.requireCsrfProtectionMatcher);
			}
			filterChain.doFilter(request, response);
			return;
		}
        // 4. request로 부터 csrf token 추출
		String actualToken = request.getHeader(csrfToken.getHeaderName());
		if (actualToken == null) {
			actualToken = request.getParameter(csrfToken.getParameterName());
		}
        
        // 5. 서버에서 생성한 csrf token과 request로 받은 csrf token 비교
		if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
			this.logger.debug(
					LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
			AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
					: new MissingCsrfTokenException(actualToken);
			this.accessDeniedHandler.handle(request, response, exception);
			return;
		}
		filterChain.doFilter(request, response);
	}
  • token이 일치하지 않으면 post,put,delete는 403에러가 발생

설정

  • @EnableWebSecurity을 사용한다면 csrf공격을 방지할 수 있음

  • HttpOnly가 체크된 쿠키는 자바스크립트로 컨트롤 할 수 없기 때문에 .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())를 명시해주어야 함

	public static CookieCsrfTokenRepository withHttpOnlyFalse() {
		CookieCsrfTokenRepository result = new CookieCsrfTokenRepository();
		result.setCookieHttpOnly(false);
		return result;
	}
  • request를 할 때 파라미터로 _csrf 혹은 헤더에 X-XSRF-TOKEN을 보내면 된다.
public final class CookieCsrfTokenRepository implements CsrfTokenRepository {

	static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";

	static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";

	static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
profile
금꽁치's Blog
post-custom-banner

0개의 댓글