CsrfFilter

midas·2022년 5월 22일
0

🔎 CSRF란?

CSRF : Cross-stie request forgery

사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를 특정 웹사이트에 요청하게 하는 공격

🔑 해결 방법?

  • Referrer 검증
    요청이 담긴 페이지가 정상적으로 제공된 페이지인지 검증!
    (도메인이 일치하는지 → 변조된 페이지가 아닌지 확인)
  • CSRF Token 활용
    ()

📌 CsrfFilter

요청이 리소스를 변경해야 하는 요청인지 확인하고, 맞으면 CSRF 토큰 검증
(기본 활성화)

  • CsrfTokenRepository
    • CSRF 토큰 저장소 인터페이스
    • HttpSessionCsrfTokenRepository 구현체 클래스가 사용 됨
public final class CsrfFilter extends OncePerRequestFilter {

	// ✨ 맨 밑에 정의되어 있음
	public static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher();

  	// ✨ HttpSessionCsrfTokenRepository 구현체 클래스가 사용됩니다.
	private final CsrfTokenRepository tokenRepository;

	private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		request.setAttribute(HttpServletResponse.class.getName(), response);

    	// ✨ Csrf 발행된 토큰이 있는지 확인하고 가져옴!
		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		boolean missingToken = (csrfToken == null);

    	// ✨ Csrf 토큰이 없으면 생성!
		if (missingToken) {
			csrfToken = this.tokenRepository.generateToken(request);
			this.tokenRepository.saveToken(csrfToken, request, response);
		}
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);

    	// ✨ 리소스가 읽기 전용일 요청이라면 허용!
		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;
		}

    	// ✨ 리소스가 변경될 수 있는 요청인 경우에는 여기서 검증을 하게 됩니다!
		String actualToken = request.getHeader(csrfToken.getHeaderName());
		if (actualToken == null) {
			actualToken = request.getParameter(csrfToken.getParameterName());
		}

    	// ✨ 일치하지 않는다면 AccessDeniedException 예외 처리하는 것을 알 수 있습니다!
		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);
	}

	/**
	 * Constant time comparison to prevent against timing attacks.
	 * @param expected
	 * @param actual
	 * @return
	 */
	private static boolean equalsConstantTime(String expected, String actual) {
		if (expected == actual) {
			return true;
		}
		if (expected == null || actual == null) {
			return false;
		}
		// Encode after ensure that the string is not null
		byte[] expectedBytes = Utf8.encode(expected);
		byte[] actualBytes = Utf8.encode(actual);
		return MessageDigest.isEqual(expectedBytes, actualBytes);
	}

	private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {

    	// ✨ 읽기 전용인 요청에 대해서는 PASS 시킨다.
    	//    → 리소스를 변경시키는 요청의 경우는 검증
		private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));

		@Override
		public boolean matches(HttpServletRequest request) {
			return !this.allowedMethods.contains(request.getMethod());
		}

		@Override
		public String toString() {
			return "CsrfNotRequired " + this.allowedMethods;
		}

	}

}

🖼 Front 살펴보기

위에서 살펴보면 `hidden` 속성으로 csrf 값이 포함된것을 볼 수 있습니다.
저 value 값을 변경해서 로그인을 하게 된다면 `access denied`가 발생합니다!

이렇게 마음대로 변경해서 요청을 하게 되면?

위와 같이 Access Denied가 발생하는 것을 볼 수 있습니다.

🔖 참고

profile
BackEnd 개발 일기

0개의 댓글