Development/Daily Error

일간에러) Spring Session과 Cookie SameSite 정책

📝 작성 : 2022.04.01  ⏱ 수정 : 
반응형

Intro

SpringBoot 1.5 버전에서 2.5 버전으로 마이그레이션 작업이 대부분 완료 되었는데 SAML을 이용한 SSO 로그인 부분에서 굉장히 오래동안 해결하지 못하던 문제가 있었습니다.

분명 해당 부분의 코드는 전혀 변경이 없었는데 단지 스프링부트 버전이 달라졌다고 해서 예전 버전에서는 되고 지금은 안되는게 통 이해가 되지 않았습니다.

문제

일단 원인은 본 글의 제목처럼 Spring Session에서의 Cookie SameSite 정책 변경 때문이었습니다.

왜 이게 문제가 되었고 스프링부트 버전과 Cookie 정책이 어떤 연관관계가 있으며 어떻게 해결을 할 수 있는지에 대해 알아보겠습니다.

로그인 성공과 실패시 요청의 차이

image-20220331150932262

SSO 로그인에 성공하던 요청

image-20220331140008876

SSO 로그인에 실패하고 있는 요청

일단 Set-Cookie를 할 때 차이점이 눈에 띄었는데요, 따로 설정한 적 없는 SameSite=Lax가 들어가 있었습니다.

그리고 또 특이한점은 Spring Boot 2.0 부터 Cookie value가 base64로 인코딩 된다는 건데요

image-20220331153523944

https://github.com/spring-projects/spring-session/issues/736

원래는 옵션으로 제공 되어 왔지만, 2.0 버전부터는 base64로 인코딩 되는게 기본 설정이 되었습니다.

CookieSerializer 에서 setUseBase64Encoding(false) 설정을 하면 해당 옵션을 끌 수 있는데요, 일단 인코딩 옵션을 비활성화 해도 상황이 개선되지는 않았습니다. 인코딩이 문제는 아니었습니다.

SameSite Attribute

2021년 말 구글의 서드파티 쿠키 제한이라는 엄청나게 큰 이슈가 있었는데요.

구글에서 방금 기계식 키보드를 검색 했는데, 불과 몇 초 후에 페이스북에서 온갖 회사들의 기계식 키보드 광고가 뜨는 섬뜩한 경험을 모두들 해 보셨을 겁니다. 이런 맞춤형 광고가 가능한 이유는 서드파티 쿠키 때문인데요, 소비자가 접속한 해당 사이트에서 직접 소비자의 정보를 수집하는걸 퍼스트 파트 쿠키 라고 하는데요 구글에서 내가 검색한 내용을 페이스북에서 활용하는건 서드 파티 쿠키 라고 합니다.

간단하게 비유하면 구글이 직접 구글에 심은 쿠키는 퍼스트파티, 페이스북이 구글 웹사이트에 심어놓은건 서드파티 쿠키라고 보면 됩니다.

iOS 14.5 버전부터 시작된 사생활 보호 조치에 이어 구글에서도 소비자들의 사생활을 존중해주는 정책을 택하면서 모두 아시는 것 처럼 광고수입에 의존하던 대표적인 기업인 페이스북은 주가가 폭락하고 미래가 불투명해지며 사명까지 변경하게 되는 사태에 도달했습니다.

HTTP response header의 Set-Cookie에 작성하는 SameSite 속성은 특정 쿠키가 퍼스트 파티 혹은 같은 사이트 컨텍스트 내에서만 접근되도록 제한할 수 있게 해줍니다. 최근 Mozilla 재단의 SameSite에 대한 정책 변경 내용을 살펴보면

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite

  • SameSite 속성이 설정되어 있지 않은 쿠키는 SameSite=Lax로 설정 된 것으로 간주된다. (변경전에는 None으로 간주)
  • SameSite 속성이 None으로 설정된 쿠키는 반드시 Secure 속성을 사용해야 한다. Https
  • 같은 도메인의 쿠키라고 해도 스키마가 다르면(Http/Https) 더이상 Same Site로 간주되지 않는다.

SameSite Value

SameSite 속성으로는 3가지 Value가 들어갈 수 있습니다.

Strict

가장 강력한 제한 설정 입니다.

쿠키는 First-party Context 내에서만 전송 되며 서드파티 웹사이트에서 시작한 Request로는 해당 쿠키가 전송되지 않습니다.

Lax

SameSite 속성이 명시적으로 작성되지 않은 경우 최신 브라우저들에서는 기본 값으로 설정되는 속성 입니다.

아래의 Defaults to Lax 항목을 통해 브라우저별 지원 현황을 확인 할 수 있습니다.

image-20220401114802286

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#browser_compatibility

해당 속성에서는 Strict 설정에서 일부 예외를 허용해 안전한 HTTP 메서드 하에서 top-level 네비게이션 일 때만 쿠키가 전송됩니다.

https://www.chromium.org/administrators/policy-list-3/cookie-legacy-samesite-policies/

  • 안전한 HTTP 메서드는 서버의 상태를 바꾸지 않는 GET등을 말합니다. POST 혹은 DELETE 등의 경우에는 안전하지 않다고 여겨집니다.

  • Top Level Navigation은 a href 링크나 302 리다이렉트, window.location.replace등 이 있습니다.

None

First-Party 혹은 Cross-Origin 리퀘스트에 모두 쿠키가 전송 됩니다.

다만 SameSite=None 일 때에는 Secure 속성이 반드시 설정되어야 하며 그렇지 않으면 해당 쿠키는 차단되어 사용 할 수 없습니다.

원인

SameSite

SameSite 속성이 Lax로 변경된 문제를 추적해 보았습니다.

org.springframework.session.web.http.DefaultCookieSerializer 내부를 들여다보면

image-20220331161108517

SameSite 변수에 "Lax"가 기본 값으로 되어 있는데요

image-20220331161206964

최종적으로 쿠키 값을 작성 할 때에, sameSite 변수가 null이 아니면 헤더값에 ; SameSite=this.sameSite를 하는데, 기본값이 정해져 있기 때문에 Lax로 들어가고 있었습니다.

image-20220331161329455

그걸 방지하기 위해선 DefaultCookieSerializer 에서 setSameSite를 호출해 다른 값으로 변경 해 주어야 합니다.

해결

SameSite=None;

CookieSerializer를 Bean으로 등록 할 때 DefaultCookieSerializer를 생성 해 sameSite 값을 변경 해 줍니다.

@Bean
public CookieSerializer cookieSerializer() throws MalformedURLException {
    DefaultCookieSerializer serializer = new DefaultCookieSerializer();
    serializer.setCookieName(sessionCookieName);
    serializer.setSameSite("None");
    return serializer;
}

SameSite 속성으로는 None, Lax, Strict가 있습니다.

처음에는 None으로 설정 해 보았는데요 Firefox 에서 아무 문제없이 작동 하길래 고쳤다고 생각 했는데

image-20220331163120838

Chrome

크롬에서는 해당 쿠키를 아에 사용할 수가 없었습니다. SameSite=None 으로 설정 한 경우에는 보안상 취약하기 때문에 Secure 옵션을 켜야만 해당 쿠키를 사용 할 수 있다고 합니다. 크롬 브라우저에서 작년쯤 변경된 정책입니다.

그래서 다시 firefox도 확인 해 보니, 같은 내용의 경고 문구를 내보내고 있었습니다.

Cookie “DRSESSION” will be soon rejected because it has the “SameSite” attribute set to “None” or an invalid value, without the “secure” attribute. To know more about the “SameSite“ attribute, read https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite

SameSite=None; sucure;

serializer.setUseSecureCookie(true);

이때는 setUseSecureCookie를 true로 지정하는 방법이 있습니다. 그러면 쿠키를 https 를 통해서만 전달하며

SameSite 속성을 None으로 사용 할 수 있게 됩니다.

image-20220331163421922

해당 쿠키의 속성이 Secure에 체크도 되었고, SameSite 속성도 None으로 설정 되었습니다.

이 경우에는 로그인도 되고 문제가 없었습니다. 로컬에서 http 환경임에도 불구하고 Secure 설정된 쿠키가 전달되는 것 으로 봐선 테스트 환경을 고려해서 주소가 localhost 인 경우에는 Secure 속성이 있어도 http환경에서도 해당 쿠키를 사용 할 수 있게끔 브라우저에서 처리한 것으로 보입니다.

하지만 문제는 해당 프로젝트를 https 에서 사용하는 경우가 대부분이지만 간혹 내부망에서만 사용되는 경우에는 http 프로토콜을 이용한 다는 것 입니다. localhost가 아닌 http 도메인에서 쿠키의 Secure 속성을 체크해봤더니 바로 문제가 발생했습니다.

SameSite=;

http 프로토콜을 사용한다면 쿠키의 secure 옵션을 사용 할 수는 없습니다.

image-20220331163751116

위에 보이는 것 처럼 setUseSecureCookie 메서드는 애초에 기본값으로 HttpServletRequest을 확인 하기 때문에 따로 설정 해 줄 필요는 없는데요. http 환경에서는 secure 속성이 자동으로 false로 들어가기 때문에 강제로 지정 해 준 것이었습니다.

하지만 지금 저희는 http 환경에서는 Chrome 때문에 SameSite 속성에 secure 없이는 None을 사용하지 못하는 상태 입니다.

이때는 Strict, Lax, None을 다 못쓰는 곤란한 상황인데요

serializer.setSameSite("");

SameSite 속성에 빈 텍스트를 전달 해 주는 방법이 있습니다.

이 경우에는 sameSite 프로퍼티가 null이 아니기 때문에 헤더에는 ; SameSite= 가 붙게 되는데요

image-20220331164408551

image-20220331164214178

이때는 스프링부트 1.5 에서 작동했던 것 처럼 SameSite가 비어있는 상태를 만들 수 있습니다. 저는 이렇게 하니 정상적으로 작동 했습니다.

Chrome 브라우저가 80 버전부터는 SameSite 속성이 없는 쿠키를 Lax로 인식한다고 알려저서 문제가 혹 생길까 싶었는데 다행히도 SameSite를 비워서 서버에서 생성을 요청한 쿠키에 브라우저라 따로 Lax속성을 붙이거나 하지는 않았습니다.

브라우저에서 자바스크립트로 쿠키를 작성할 때도 SameSite 관련 내용을 기입하지 않는다고 해서 SameSite 속성이 자동으로 붙거나 하지는 않더라고요. Lax 속성을 붙이는 건 아니고 브라우저 내에서만 Lax 속성이 있는 걸로 간주하는 것 으로 보입니다.

이상입니다.

반응형