웹 분야에서 쿠키는 상태 관리 메커니즘으로 사용되어 왔습니다.
"쿠키"라는 이름은 헨젤과 그레텔에서 길을 다시 찾을 때 쿠키를 사용한 것에서 유래되었다고 합니다.
기본적으로 HTTP 프로토콜은 stateless 합니다. 즉, Http 요청 자체에서 이전에 오갔던 대화 내용을 포함하지 않습니다.
만약에 상태 관리가 이루어지지 않는다면, 다음과 같은 상황이 일어날 수 있겠죠.
서버에서 세션을 쓰더라도 세션ID를 클라이언트 쪽에서 보관해야되기 때문에 쿠키가 없다면 세션을 사용할 수 없습니다.
오늘은 쿠키🍪에 대해서 자세하게 알아봅시다. 🤗
IETF(국제 인터넷 표준화 기구)에서 규정한 문서 RFC 6265에 쿠키와 Set-Cookie 헤더에 대한 표준을 정의하고 있습니다.
즉, 서버에서 클라이언트로 상태 정보를 보내는 방법을 설명합니다.
해당 문서를 아주 간단하게 요약하자면,
서버에서 클라이언트가 상태 정보를 저장하는 것을 원할 때 Set-Cookie 헤더를 통해 응답을 내보내야 하고,
클라이언트는 Http 상태코드가 100번대를 제외하고 Set-Cookie 헤더를 무시하지 않는 것을 권장합니다.
그럼 Set-Cookie는 어떤 형식이어야 할까요?
Set-Cookie 헤더의 Syntax, 즉 문법은 다음과 같습니다
set-cookie-header = "Set-Cookie:" SP set-cookie-string
set-cookie-string = cookie-pair *( ";" SP cookie-av )
cookie-pair = cookie-name "=" cookie-value
cookie-name = token
cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
; US-ASCII characters excluding CTLs,
; whitespace DQUOTE, comma, semicolon,
; and backslash
token = <token, defined in [RFC2616], Section 2.2>
cookie-av = expires-av / max-age-av / domain-av /
path-av / secure-av / httponly-av /
extension-av
expires-av = "Expires=" sane-cookie-date
sane-cookie-date = <rfc1123-date, defined in [RFC2616], Section 3.3.1>
max-age-av = "Max-Age=" non-zero-digit *DIGIT
; In practice, both expires-av and max-age-av
; are limited to dates representable by the
; user agent.
non-zero-digit = %x31-39
; digits 1 through 9
domain-av = "Domain=" domain-value
domain-value = <subdomain>
; defined in [RFC1034], Section 3.5, as
; enhanced by [RFC1123], Section 2.1
path-av = "Path=" path-value
path-value = <any CHAR except CTLs or ";">
secure-av = "Secure"
httponly-av = "HttpOnly"
extension-av = <any CHAR except CTLs or ";">
쉽게 설명하자면 Set-Cookie 헤더는 다음과 같이 사용됩니다.
Set-Cookie: name=value; Expires=...; Max-Age=...; Domain=...; Path=...; Secure; HttpOnly
Set-Cookie 헤더 1개에 1개의 쿠키만 설정할 수 있으며, 여러 개의 쿠키를 저장해야 될 경우 여러개의 Set-Cookie 헤더를 사용할 것을 권장하고 있습니다.
Origin servers SHOULD NOT fold multiple Set-Cookie header fields into a single header field.
The usual mechanism for folding HTTP headers fields (i.e., as defined in [RFC2616]) might change the semantics of the Set-Cookie header field because the %x2C (",") character is used by Set-Cookie in a way that conflicts with such folding.
이유를 살펴보면, Expires
을 설정할 때 사용하는 rfc1123-date
형식에 "," 가 사용되기 때문입니다.
rfc1123-date = wkday "," SP date1 SP time SP "GMT"
이제 쿠키 속성에 대해서 하나하나 살펴봅시다.
기본적으로 클라이언트는 사용 가능한(만료되지 않은) 쿠키를 서버에 요청을 보낼 때 같이 포함시킵니다.
여기서 사용 가능한 쿠키를 판단할 때, Set-Cookie 헤더에 포함된 속성들을 이용하게 됩니다.
즉, 쿠키가 저장될 때 쿠키의 속성들도 함께 저장되는 것입니다.
하지만 주의해야될 점은 쿠키는 [이름, 도메인, 경로] 값으로 구분하기 때문에 같은 [이름, 도메인, 경로]를 가진 쿠키가 Set-Cookie로 새로 들어온다면 값은 덮어쓰여집니다. 이를 이용해 서버는 기존의 쿠키를 삭제할 수 있습니다.
쿠키의 만료기한을 설정하는 속성입니다.
만료기한은 날짜와 시간으로 설정할 수 있습니다.
클라이언트는 만료기한이 지난 쿠키들을 들고 있을 필요가 없으며, 실제로 메모리 최적화나 보안 문제로 인하여 바로 삭제한다고 합니다.
Expires
와 마찬가지로 쿠키의 만료기한을 설정하는 속성인데, Expires
와는 다르게 초 단위로 설정하게 됩니다.
음수나 0으로 값이 주어지게 되면, 바로 쿠키를 만료시킵니다. 이를 이용해서 서버가 쿠키를 삭제할 수 있습니다.
주의: 일반적으로 많은 프레임워크들이 개발의 편리성을 제공하기 위해, setMaxAge(-1)
은 Max-Age
가 설정되지 않게 하여 세션 쿠키가 됩니다. 즉시 만료되게 하고 싶으면 setMaxAge(0)
으로 해주세요.
Expires
와 마찬가지로 쿠키가 만료가 되면 클라이언트는 해당 쿠키를 들고 있을 필요가 없습니다.
다만, 브라우저에 따라서 Max-Age
를 지원하지 않을 수도 있으니 주의가 필요합니다.
Expires
와 Max-Age
속성이 동시에 설정되면, Max-Age
가 우선순위를 가집니다. 만약 두 속성 모두 없다면, 쿠키는 클라이언트가 정의하는 세션이 끝날때까지만 유지되는 세션 쿠키가 됩니다.
흔히 말하는 영구 쿠키는 만료기한이 정해진 쿠키, 즉 세션 쿠키가 아닌 쿠키입니다. 영구 쿠키는 브라우저의 세션이 종료되어도 남아있다는 특징을 가집니다.
해당 쿠키를 특정 도메인으로 보내는 요청에만 포함시키게 하는 속성입니다.
만약 Set-Cookie 헤더에 Domain
속성이 없더라도 응답을 보낸 서버의 도메인을 Domain
으로 인식합니다.
또한, 응답을 보낸 서버의 도메인과 Domain
의 값이 동일한 쿠키만 저장한다는 특징이 있습니다.
Domain
과 비슷한 원리로 특정 경로에만 쿠키를 포함시키고 싶을 경우 해당 속성을 이용합니다.
Path
정보가 누락된 경우에는 요청 URI의 경로 정보를 기본값으로 설정합니다.
Secure connection에서만 쿠키를 포함시키고 싶은 경우 Secure
을 설정하게 됩니다.
쿠키는 HTTPS 요청에만 포함되며 HTTP 요청에는 포함되지 않습니다.
Secure
설정을 하게 되면 쿠키의 기밀성은 보장되어도, HTTP 요청으로 덮어쓰여질 수 있기 때문에 만능은 아닙니다.
쿠키의 사용 범위를 HTTP 요청에만 국한시키는 설정입니다.
HttpOnly
쿠키는 non-HTTP API를 통해 접근이 불가능하게 됩니다.
예를 들어, 자바 스크립트를 통해 쿠키의 값을 노출시키는 행위가 불가능해집니다.
Secure
과 HttpOnly
는 독립된 속성이며, 하나의 쿠키가 두 속성 모두 가질 수 있다는 점을 유의해야 합니다.
SameSite
는 RFC 6265의 공식 스펙이 아닙니다!!
구글에서 제안한 draft에 포함된 내용으로, 받아들여진다면 RFC 6265를 대체하게 됩니다.
아직 표준은 아니지만, 구글이 제안했기에 구글 크롬을 비롯한 많은 브라우저에서 지원하는 쿠키의 속성입니다. 브라우저별 지원 현황은 해당 링크를 참고해주세요
SameSite
속성은 enforcement
의 값을 조정하며, cross-site 문맥에서 CSRF 공격을 막기 위해 사용됩니다.
enforcement
의 기본값은 Default
이며, None
, Lax
, Strict
의 값을 가질 수 있습니다.
흔히 SameSite
의 기본값을 Lax
로 알고 있는데, 브라우저마다 달라서 주의가 필요한 부분입니다. (RFC 6265bis에는 Lax
라고 하지만 표준이 아니라서 생기는 문제인 것 같습니다.)
Strict
: "same-site" 요청에만 쿠키를 포함시키게 됩니다.Lax
: "same-site" 요청과 safe HTTP method를 사용하는 "cross-site" top-level navigation에서만 쿠키를 포함시키게 됩니다.None
: "same-site" 요청과 "cross-site" 요청 모두에 쿠키를 포함시킬 수 있습니다. 하지만, HTTPS에서만 가능한 속성으로 Secure
속성이 있어야 합니다.SameSite
의 값은 Domain
과 마찬가지로 쿠키의 포함 여부뿐만 아니라 쿠키 생성 여부에도 관여합니다.
safe HTTP method를 사용하는 "cross-site" top-level navigation?? 🤔
safe HTTP method란, RFC 7231에서 정의하는 Read-Only method로 GET
, HEAD
, OPTIONS
, TRACE
가 있습니다.
"cross-site"는 "same-site"가 아님을 의미합니다.
top-level navigation은 쉽게 말해 URL이 바뀌는 이동을 뜻합니다. iframe의 navigation과 구분됩니다.
그럼, "same-site"와 "cross-site"를 어떻게 구분할까요?
구분하는 알고리즘은 해당 문서에 정의되어 있으며 다음과 같습니다.
Two origins, A and B, are considered same-site if the following algorithm returns true:
1. If A and B are both the same globally unique identifier, return true.
2. If A and B are both scheme/host/port triples:
1. If A's scheme does not equal B's scheme, return false.
2. Let hostA be A's host, and hostB be B's host.
3. If hostA equals hostB and hostA's registrable domain is null, return true.
4. If hostA's registrable domain equals hostB's registrable domain and is non-null, return true.
3. Return false.
요약하자면 same-site이기 위해서 scheme, domain, port 3개가 같아야합니다. SOP, CORS에서 Same Origin을 판단하는 방법보다는 조금 느슨합니다.
Two origins are "the same" if, and only if, they are identical. In
particular:
o If the two origins are scheme/host/port triples, the two origins
are the same if, and only if, they have identical schemes, hosts,
and ports.
o An origin that is a globally unique identifier cannot be the same
as an origin that is a scheme/host/port triple.
예를 들어,
Origin A: https://www.example.com:443
Origin B: https://foo.example.com:443
일 때
Origin A와 B는 "same-site"이며 "cross-origin"입니다. 즉, "same-site"를 판단하기 위해서 중요한 부분은 eTLD + 1
입니다.
마지막으로, 클라이언트(브라우저, user agent)를 구현할 때 고려해야되는 사항에 대해 간단하게 알아보겠습니다.
지금까지 쿠키가 왜 필요하고, 쿠키의 속성에는 어떠한 것들이 있는 지 살펴보았습니다.
특히 새롭게 제안된 SameSite 속성에 대해서 자세히 알아보았습니다.
쿠키를 사용하게 되는 취약점(csrf, session fixation 등)에 대해 더 알아보시면 많은 도움될 것 같습니다.