악용 보호

enxnong·2024년 6월 20일
0

스프링 시큐리티

목록 보기
6/13

정수원님의 강의 스프링 시큐리티 완전 정복 [6.x 개정판] 보면서 공부한 내용입니다.

CORS (Cross Origin Resource Sharing)

  • 교차 출처 리소스 공유로 해당 웹 페이지의 출처로 나온 리소스가 아닌 다른 웹 페이지의 리소스를 사용하고자할 경우에 등장한다
  • 특별한 HTTP 헤더를 통해 다른 출처의 리소스에 접근할 수 있도록 '허가'를 구하는 방식이다
  • 브라우저는 클라이언트의 요청 헤더와 서버의 응답헤더를 비교하여 공유 가능 여부를 결정한다
  • URL 구성요소 중 Protocol, Host, Port가 모두 동일할 경우 동일한 출처로 구분되고 세가지 중 하나라도 틀린 경우 다른 출처로 구분된다

CORS의 종류

  • Simple Request
    • 예비 요청 과정 없이 자동으로 CORS가 작동하여 서버에 본 요청을 한 후, 서버가 응답의 헤더에 Access-Control-Allow-Origin과 같은 값을 전송하면 브라우저가 서로 비교 후 CORS 정책 위반여부를 검사한다
// 요청 받을 url만 명시
Origin: https:// security.io 
  • Simple Request 조건 만족

  • Simple Request

💡 제약 사항

  • GET, POST, HEAD 중 한 가지 METHOD를 사용해야 한다.
  • 헤더는 Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width Width 만 가능하다
  • Content-type 은 application/x-www-form-urlencoded, multipart/form-data, text/plain 만 가능하다
  • Preflight Request (예비요청)
    • 요청을 한 번에 보내지 않고, 예비 요청을 보낸 후 결과값에 따라 본 요청 전송 유무를 판단한다. 즉, 예비 요청과 본 요청으로 나누어 서버에 전달하고 예비 요청의 메소드에는 OPTIONS가 사용된다
    • 즉, 예비 요청을 통해 출처가 다른 두 서비스 간의 자원 공유가 가능하다면 본 요청을 전송하여 응답을 하는 방식이다
    • 쿠키(sessionId)가 포함되어 있지 않기 때문에 Spring Security 이전에 처리해야한다
// 예비 요청 시 포함
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: GET

💡 OPTIONS
클라이언트가 서버로부터 요청을 했을 때 해당 서버가 받은 요청에 대해서 지원하는 HTTP 메소드가 어떤 것인지 미리 탐색을 하고자 할 때 사용

ex) 서버가 지원하는 HTTP 메소드가 GET 방식만 지원한다하는데 클라이언트가 POST 방식으로 보내면 실패하므로 어떤 방식을 지원하는지 미리 확인하는 것이 OPTIONS다

💡 CORS가 먼저 처리되도록 CorsFilter을 사용하여 Spring Security와 통합할 수 있다

  • 사전 요청

  • 본 요청

동일 출처 기준

Access-Control-Allow-* 세팅 종류

  • Access-Control-Allow-Origin
    • 헤더에 작성된 출처만 리소스에 접근할 수 있도록 허용
    • ex) * (모든 도메인 가능), https://security.io
    • 허용 도메인이 다른 경우

  • Access-Control-Allow-Methods
    • preflight request 에 대한 응답으로 실제 요청 중에 사용할 수 있는 메서드
    • GET, POST, HEAD, OPTIONS, *
  • Access-Control-Allow-Headers
    • 실제 요청 중에 사용 가능한 헤더 필드 이름
    • Origin,Accept,X-Requested-With,Content-Type, Access-Control-Request-Method,Access-Control-Request-Headers, Custom Header, *
  • Access-Control-Allow-Credentials
    • 실제 요청(출처로부터) 보안과 관련된 쿠키나 인증 등의 요소들이 포함되어서 왔을 때 허용 여부를 설정한다
    • false (차단), true (허용)
  • Access-Control-Max-Age
    • 예비 요청 결과를 캐시 할 수 있는 시간을 나타내는 것으로 해당 시간동안은 예비 요청을 다시 하지 않게 된다

CSRF (Cross Site Request Forgery)

  • 공격자가 사용자로 하여금 이미 인증된 다른 사이트에 원치 않는 작업을 수행하도록 하는 기법
  • 사용자의 인증 정보(쿠키 및 인증 세션)을 이용하여 사용자가 의도하지 않은 서버로 전송하게 한다
  • 웹 사이트 방문 또는 링크 클릭 할 때도 발생할 수 있다

CSRF 기능 활성화

  • csrf 토큰은 서버에 의해 생성되어 사용자의 세션에 저장되고 폼을 통해 서버로 전송되는 모든 변경 요청에 포함되어야 하며 서버는 이 토큰은 검증하여 요청의 유효성을 확인한다
  • POST, PUT, DELETE 와 같은 벼경 요청 메서드마 CSRF 토큰 검사를 수행한다
  • 실제 CSRF 토큰이 브라우저에서 자동으로 포함되지 않는 요청 부분(쿠키를 토큰으로 만들면 안됨)에 위치해야 하며 HTTP 매개변수나 헤더에 실제 CSRF 토큰을 요구하는 것이 효과적이다

💡 쿠키는 브라우저에서 자동으로 요청하기 때문에 쿠키를 토큰으로 만드는 것은 보안에 위험하다

@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
 http.csrf(Customizer.withDefaults()); // 별도 설정없이 활성화 상태로 초기화 된다
 return http.build();
}

CSRF 기능 비활성화

  • 전체 비활성화
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/csrf").permitAll()
                    .anyRequest().authenticated())
                .formLogin(Customizer.withDefaults())
//                .csrf(csrf -> csrf.disable()) // csrf 전체 비활성화
                .csrf(csrf -> csrf.ignoringRequestMatchers("/csrf")) 
                // /csrf url만 기능 비활성화
            ;
        return http.build();
    }

CSRF 기능 출력

  • 아래 화면이 뜨면 CSRF로 인해 접근 거부당한 것

  • 인증

CSRF 토큰 유지

  • 토큰을 생성한 후 저장소에 보관하여 저장하는 것
  • HttpSessionCsrfTokenRepository 와 CookieCsrfTokenRepository 를 지원한다

HttpSessionCsrfTokenRepository - 세션에 토큰 저장

@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
 HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
 http.csrf(csrf -> csrf.csrfTokenRepository(repository));
 return http.build();
 }

CookieCsrfTokenRepository - 쿠키에 토큰 저장

@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
 CookieCsrfTokenRepository repository = new CookieCsrfTokenRepository();

// 첫 번째 방법
http.csrf(csrf -> csrf.csrfTokenRepository(repository));

// 두 번째 방법
// http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));

 return http.build();
 }

CsrfTokenRequestHandler

  • 토큰을 생성 및 응답하고 HTTP 헤더 또는 요청 매개변수로부터 토큰의 유효성을 검증하도록 한다
  • XorCsrfTokenRequestAttributeHandler 와 CsrfTokenRequestAttributeHandler 를 제공하며 사용자 정의 핸들러를 구현할 수 있다
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
 XorCsrfTokenRequestAttributeHandler csrfTokenHandler = new XorCsrfTokenRequestAttributeHandler();
 http.csrf(csrf -> csrf.csrfTokenRequestHandler(csrfTokenHandler));
 return http.build();
}

CSRF 지연 로딩

  • CSRF 토큰을 매 요청마다 꺼내오는 것이 아닌 꼭 필요한 경우에 꺼내오도록 하는 것이다
  • 즉, CSRF 토큰은 세션에 저장되어 있기 때문에 매 요청마다 토큰을 가져올 필요가 없어 성능을 향상시킬 수 있다
  • POST, DELETE 등 안전하지 않은 HTTP 메서드를 사용하여 요청을 발생할 때와 CSRF 토큰을 응답에 렌더링하는 모든 요청에서 필요하기 때문에 그 외 요청에는 지연로딩을 사용한다
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
 XorCsrfTokenRequestAttributeHandler handler = new XorCsrfTokenRequestAttributeHandler();
 handler.setCsrfRequestAttributeName(null); 
 // 바로 CSRF 토큰을 모든 요청마다 가져온다
 http.csrf(csrf -> csrf
 .csrfTokenRequestHandler(handler));
 return http.build();
}

  • 지연 객체로부터 실제 객체를 가져오기 위해 세션에 저장이 되어있으면 토큰값을 가져오고 저장되지 않아있다면 새로 생성한다
@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

// 지연된 객체 => request 에 저장됨
		DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);
		request.setAttribute(DeferredCsrfToken.class.getName(), deferredCsrfToken);
		this.requestHandler.handle(request, response, deferredCsrfToken::get);
		
// false 인 경우
// GET 방식인 경우 => CSRF 기능 실행 X        
        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;
		}
        
// POST, DELETE 등 방식인 경우 => CSRF 기능 실행 O        
		CsrfToken csrfToken = deferredCsrfToken.get();
		String actualToken = this.requestHandler.resolveCsrfTokenValue(request, csrfToken);
		if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
			boolean missingToken = deferredCsrfToken.isGenerated();
			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);
	}

CSRF 토큰 저장

  • 쿠키 생성 (http-only 버전)
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        // 쿠키로 저장
        CookieCsrfTokenRepository csrfTokenRepository = new CookieCsrfTokenRepository();

        // 기본적인 쿠키 이름
//        static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";

        http
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/csrf").permitAll()
                    .anyRequest().authenticated())
                .formLogin(Customizer.withDefaults())
                .csrf(csrf -> csrf.csrfTokenRepository(csrfTokenRepository))
            ;

        return http.build();
    }

  • 쿠키 생성 (스크립트에서도 읽도록 가능한 버전)
 @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        // 쿠키로 저장
        CookieCsrfTokenRepository csrfTokenRepository = new CookieCsrfTokenRepository();

        // 기본적인 쿠키 이름
//        static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";

        http
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/csrf").permitAll()
                    .anyRequest().authenticated())
                .formLogin(Customizer.withDefaults())
                // XSRF-TOKEN은 http 통신에서만 사용할 수 있으므로 withHttpOnlyFalse를 활용하여 토큰을 스크립트에서 읽을 수 있도록 설정
                .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
            ;

        return http.build();
    }

  • 세션에서 가져옴

    @GetMapping("/csrfToken")
    public String csrfToken(HttpServletRequest request) {

        // request에서 토큰의 이름(CsrfToken.class.getName())과 문자열(_csrf)로
        // 지연된 객체가 저장됐으므로 참조해서 가져오기
        CsrfToken csrfToken1 = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        CsrfToken csrfToken2 = (CsrfToken) request.getAttribute("_csrf");
        String token = csrfToken1.getToken();
        return token;
    }
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/csrf","/csrfToken").permitAll()
                    .anyRequest().authenticated())
                .formLogin(Customizer.withDefaults())
                // XSRF-TOKEN은 http 통신에서만 사용할 수 있으므로 withHttpOnlyFalse를 활용하여 토큰을 스크립트에서 읽을 수 있도록 설정
//                .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
            ;

        return http.build();
    }

SameSite

  • SameSite는 최신 방식읜 CSRF 공격 방어 방법 중 하나로 서버가 쿠키를 설정할 때 SameSite 속성을 지정하여 크로스 사이트 간 쿠키 전송에 대한 제어를 핸들링 할 수 있다

SameSite 속성

  • Strict
    • 동일 사이트에서 오는 모든 요청에 쿠키가 포함되고 크로스 사이트 간 HTTP 요청에 쿠키가 포함되지 않는다

사용자가 A 사이트에 방문하면 해당 서비스는 사용자에게 쿠키를 발급한다. 해당 쿠키는 세션 쿠키로 브라우저에 저장된다. 그렇기에 사용자가 A 사이트에서 작업을 진행하면 세션쿠키가 자동적으로 브라우저에 전달된다. 다만, 사용자가 A 사이트에서 B 사이트로 이동한 후 어떤 작업을 진행 후 다시 A 사이트로 이동한 경우에는 세션 쿠키가 브라우저에 전송되지 않는다.

  • Lax(기본 설정)
    • 동일 사이트에서 오거나 Top Level Navigation 에서 오는 요청 및 메소드가 읽기 전용인 경우 쿠키가 전송되고 그렇지 않으면 HTTP 요청에 쿠키가 포함되지 않는다
    • 사용자가 링크(<'a'>)를 클릭하거나 window.location.replace , 302 리다이렉트 등의 이동이 포함된다. 그러나 <'iframe'>이나 <'img'>를 문서에 삽입, AJAX 통신 등은 쿠키가 전송되지 않는다
    • SameSite는 요청한 사용자가 세션 쿠키가 있지만 속성이 LAX인 경우 POST로 했을 때 쿠키 자체를 브라우저에 전송하지 않아 인증에 실패하도록 한다. 결국, CSRF를 방어하는 효과가 나타난다.

💡 Top Level Navigation이란?
사용자가 링크(<'a'>)를 클릭하거나 window.location.replace , 302 리다이렉트 등의 이동

동일 사이트에서는 쿠키를 전송하지만 사용자가 B 사이트에서 쿠키를 발급한 A 사이트로 재이동한 경우 Top Level Navigation 또는 GET 방식 인 경우에는 쿠키를 전송하지만 POST, DELETE와 같은 서버의 자원이 변하는 요청에는 쿠키를 전송하지 않는다

  • None
    • 동일 사이트 및 크로스 사이트 요청의 경우에도 쿠키가 전송된다. 이 모드에서는 반드시 HTTS 의한 Secure 쿠키로 설정되야 한다
    • 즉, 아무 설정도 안한 상태이다

SameSite 적용하기

// 의존성 추가
implementation group: 'org.springframework.session', name: 'spring-session-core', version: '3.2.1'


@Configuration
@EnableSpringHttpSession
public class HttpSessionConfig {

    // 쿠키 설정
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();  // 객체 생성
        serializer.setUseHttpOnlyCookie(true); // http 통신에서만 사용
        serializer.setUseSecureCookie(true); // 보안 쿠키 사용
        serializer.setSameSite("None"); // SameSite 속성 설정
        return serializer;
    }

    // 설정한 쿠키를 저장할 수 있는 Session Repository
    @Bean
    public SessionRepository<MapSession> sessionRepository() {
        return new MapSessionRepository(new ConcurrentHashMap<>())
    }
}
  • SameSite("None")인 경우

  • SameSite("Strict")인 경우

  • SameSite("Lax")인 경우

profile
높은 곳을 향해서

0개의 댓글