1. 웹 세션과 쿠키

- 네트워크 관련해서 ‘세션’이라 함은 흔히 양 컴퓨터 사이 유한한 수명을 갖는 네트워크 연결을 뜻하며, 물리적 개념이라기보단 논리적 개념이다. 특히 웹에서는 서버-클라이언트 사이 연결을 뜻하는데, stateless한 HTTP 프로토콜의 특성으로 인해 온전히 HTTP만으로는 그 개념을 구현하는 것이 쉽지 않다. 1994년 당시 미국의 통신사 MCI가 온라인 쇼핑 웹서비스를 개발하며 쇼핑 카트 기능을 구현하길 바랐었는데, 이러한 HTTP의 특성으로 인해 그 구현이 불가능했다. 이에 MCI는 당시 웹 브라우저를 개발하고 있었던 넷스케이프 측에 이에 관한 요청을 전달했고, 그에 따라 넷스케이프는 첫 베타버전에서부터 ‘쿠키’라는 개념을 도입하여 이를 구현했다(‘쿠키’라는 이름은 당시 이미 유닉스에서 프로그램 간 통신 시 전송되던 패킷을 지칭하는 용어로 널리 사용되던 ‘magic cookie’라는 용어에서 따온 것이다).

- 구체적으로, 웹 세션은 정확히는 웹 서버에 각 클라이언트과의 연결을 구분하여 관리하기 위한 개념으로서 구현되어 있다. 웹 서버는 클라이언트로부터 새로운 세션을 생성하는 요청을 받으면 서버에 그 클라이언트를 위한 세션을 생성하고 그 세션을 식별할 수 있는 특별한 토큰을 생성해 이를 클라이언트에 ‘쿠키’로서 전송한다. 이때 클라이언트는 이를 브라우저에 저장하며, 그 후 그 브라우저가 종료되기 전까지(=세션이 유지되는 동안) 그 웹서버에 HTTP 요청을 할 때 그 쿠키를 함께 전송한다. 이때 웹서버는 그 클라이언트의 HTTP 요청 메시지에 담긴 쿠키를 통해 그 클라이언트의 세션을 확인하고 그 클라이언트에 관한 과거 정보를 활용하여 그 클라이언트를 위한 적절한 응답 메시지를 생성 및 전송한다. HTTP는 기본적으로 stateless한 프로토콜로서 stateful한 통신을 할 수 없지만, 이처럼 웹 서버와 클라이언트의 브라우저는 세션의 내부 구현과 더불어 ‘쿠키’를 이용해 stateful한 통신을 할 수 있다.

- 서버로부터 발급받은 쿠키에는 만료 날짜가 있는 쿠키가 있고 없는 쿠키가 있는데, 만료 날짜가 있는 쿠키는 ‘영구 쿠키’라 하며 이러한 쿠키는 브라우저를 종료하더라도 삭제되지 않고 있다 설정된 만료 날짜에 도달하면 자동으로 삭제되고 없는 쿠키는 ‘세션 쿠키’라 하며 사용자의 웹 브라우저 창이나 탭이 닫힐 때 소멸한다(일부 브라우저의 경우 완전히 종료하기 전까지 세션 쿠키가 유지될 수 있다). 사용자가 웹 브라우저의 설정을 통해 수동으로 쿠키를 삭제/변경하는 것도 가능하다. 또한, 클라이언트로부터 쿠키와 함께 HTTP 메시지를 수신한 서버가 클라이언트의 쿠키를 강제로 만료시키는 쿠키를 보내는 경우도 있다.

  • 일반적으로 세션의 수명을 관리하는 것은 전적으로 서버이지만, 클라이언트의 브라우저에 저장되는 쿠키의 값이 유지돼야 정확한 세션의 개념이 성립하는 바, 브라우저의 종료 등으로 인해 세션 쿠키가 소멸하면 사실상 그 세션은 종료된다.

2. 쿠키의 구조

- 웹사이트에서는 쿠키를 생성해서 클라이언트에 HTTP 응답 메시지를 보낼 때 그 헤더에 Set-Cookie라는 필드를 포함시켜 쿠키를 발급한다. 이때 이 필드를 통해 쿠키를 수신한 클라이언트의 브라우저는 이를 클라이언트의 PC에 파일 형태로 저장했다가, 이후 그 웹사이트에 추가로 HTTP 요청 메시지를 전송할 때마다 해당 쿠키 내용을 그 헤더의 Cookie 필드에 담아 전송한다.

1
2
3
4
Set-Cookie: username=JohnDoe
Set-Cookie: sessionId=abc123xyz; Path=/; Secure
Set-Cookie: language=en; Path=/; Expires=Wed, 09 Jun 2024 10:18:14 GMT
...

- 하나의 쿠키는 Set-Cookie 필드 하나에 담긴 메시지 내용으로 그 내용이 정해진다. ;를 기준으로 쿠키의 내용이 구분되는데, 맨 앞에 오는 식의 = 앞부분은 그 쿠키의 이름이고 그 뒷부분은 그 쿠키가 갖는 값을 뜻한다.

  • Path: 그 쿠키가 유효한 그 도메인 내 경로 범위를 나타낸다. 생략 가능하나, 이 경우 쿠키는 현재 문서의 경로를 기본값으로 사용한다. 즉, 그 경로 및 그 하위 경로에서만 그 쿠키가 유효하게 된다.

  • Expires: 그 쿠키의 만료 기간을 뜻한다.

  • Secure: HTTPS로 접속한 경우에만 그 쿠키를 전송하도록 하는 플래그.

  • HttpOnly: JS를 통해 쿠키값을 얻지 못하도록 하는 플래그.

- RFC 6265 표준에 따르면, 브라우저는 한 도메인당 총 50개, 모든 도메인에 대해 최소 300개의 쿠키를 저장할 수 있어야 한다. 쿠키 하나의 최대 크기는 4096 바이트(약 4KB)로 제한된다. 다만 브라우저가 항상 이 표준을 따르리라 기대하는 것은 부적절하며 너무 많은 쿠키를 사용하는 것은 서버-클라이언트 사이 통신 성능에도 악영향이 있다. 따라서 되도록 다른 클라이언트 측 저장 메커니즘(localStorage 등)을 고려할 필요가 있다.

3. 쿠키 관련 중요 이슈

1) 쿠키와 개인정보 관련 법률

- 세션을 통해 stateful한 통신이 구현돼있는 웹서버는 모두 쿠키를 사용하므로, 만약 어떤 웹사이트에서 쿠키를 사용할 것을 동의했다면 이는 내가 그 웹사이트에 접속한 한 세션 동안 했던 여러 활동에 관한 기록을 그 서버가 수집하는 것에 동의한 것과 같다. 이것이 일반 웹서비스 수요자에 대한 서비스 공급자의 지나친 개인정보 수집이 될 수 있음을 우려하여, 미국 캘리포니아 주와 EU에서는 웹사이트가 쿠키 같은 사용자 추적 기술을 사용하기에 앞서 반드시 사용자의 명시적 동의를 받도록 강제하는 CCPA, GDPR 등 법률을 입법하였고 세계적으로 이러한 법률 입법이 계속 이루어지고 있는 추세다.

2) 쿠키의 주요 보안 이슈

- 웹서버가 생성한 세션 토큰은 서버가 클라이언트를 식별하는 고유한 식별자인 바, 그 세션 토큰 값이 곧 서버에서 그 클라이언트임을 인증하는 일종의 인증키라 할 수 있다. 따라서 만약 예를 들어 어떤 웹서버에 아이디와 비밀번호를 입력하여 로그인한 후에 신용카드 비밀번호 등 중요한 정보를 그 서버에 저장했다면, 이와 같은 상황에서 세션 토큰이 제3자에게 유출되는 것은 심각한 보안사고로서 즉시 관련 정보가 제3자에게 유출된 것과 다름없다. 그러므로 브라우저를 사용하는 사람들은 중요한 웹사이트에 접속해서 로그인한 다음에는 이러한 세션 토큰 값이 유출되지 않도록 주의를 기울여야 하며, 보안에 의심이 가는 브라우저는 함부로 사용하지 않도록 굉장히 조심해야 한다. (대부분의 믿을 수 있는 브라우저는 쿠키에 의심스러운 프로그램이 함부로 접근하지 못하도록 특별한 조치를 취하고 있다.)

  • 중간자 공격과 쿠키: HTTP 프로토콜은 암호화 없이 평문으로 메시지를 전송하기 때문에 이러한 세션 토큰이 제3자에 의해 탈취되기 쉬우므로(중간자 공격), 중요한 정보를 사용하는 웹사이트를 사용할 땐 반드시 HTTPS 프로토콜을 통해 그 웹사이트에 접속해야 한다.

  • XSS 공격과 쿠키: 쿠키는 기본적으로 JS 코드를 통해서 그 값을 얻어올 수 있는데 이 경우 XSS 공격을 통해 공격자가 쿠키를 탈취할 수 있으므로, 서버 측에서는 쿠키를 생성할 때 HttpOnly 속성을 추가해서 JS 코드로 쿠키 값을 가져오지 못하도록 해 XSS 공격을 방어할 수 있다.

4. CSRF 공격과 쿠키

1) 개요

- CSRF(Cross-Site Request Forgery)는 공격자가 사용자의 신원을 도용하여 그 사용자가 인증을 거친 웹 사이트에 악의적 요청을 보내는 공격으로, ‘매 요청마다 자동으로 쿠키 내용이 함께 전달된다’라는 쿠키의 특성을 이용한 공격이다. 다음은 CSRF가 일어나는 구체적 한 사례이다.

(1) 사용자가 A 웹사이트에 로그인했고, 이에 따라 세션 ID가 쿠키로 사용자의 브라우저에 저장된다.

(2) 이후 사용자가 B 웹사이트를 방문한다. B 웹사이트에는 공격자가 만든 악의적인 코드가 포함되어 있으며, 이 코드는 A 웹사이트에 특정 요청을 보내도록 만들어져 있다. 따라서 B 웹사이트에 방문한 사용자의 브라우저는 악의적 코드를 실행한다. 이 코드는 사용자의 이름으로 A 웹사이트에 요청을 보내는데, 쿠키의 기술적 특성으로 인해 이 요청에는 사용자의 브라우저에 있는 쿠키가 함께 보내진다. 다음은 CSRF 공격을 수행하는 B 웹사이트를 구현한 코드 사례다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<html>
<head>
    <title>악의적인 사이트</title>
    <script type="text/javascript">
        window.onload = function() {
            var form = document.createElement("form");
            form.setAttribute("method", "post");
            form.setAttribute("action", "https://a-website.com/update-profile");
            
            // 예: 사용자의 이메일 주소를 변경하려는 악의적인 내용
            var hiddenField = document.createElement("input");
            hiddenField.setAttribute("type", "hidden");
            hiddenField.setAttribute("name", "email");
            hiddenField.setAttribute("value", "hacker@example.com");
            form.appendChild(hiddenField);

            document.body.appendChild(form);
            form.submit();
        }
    </script>
</head>
<body>
</body>
</html>

(3) 따라서 A 웹사이트는 이 요청을 사용자가 직접 보낸 것으로 인식하고, 적절히 인증된 사용자의 요청으로 간주하여 해당 요청을 처리한다. 이 요청은 사용자의 계정 비밀번호를 변경하거나 금액을 공격자의 계좌로 이체하는 요청일 수 있다.

- 이러한 공격은 앞서 언급했듯 쿠키가 웹페이지 요청 시 항상 함께 보내지는 특성이 있기 때문에 일어나는 공격으로, 쿠키의 보안상 허점으로 여겨지기도 한다. 이러한 공격에 대비하여, 많은 웹 서버가 클라이언트에 세션 토큰과 더불어 anti-CSRF 토큰을 함께 발급하는 등 여러 조치를 취하고 있다.

2) CSRF 공격에 대한 대응책

  • anti-CSRF 토큰: CSRF 공격을 방어하는 방법으로서 흔히 사용되는 방법 중 하나가, 클라이언트가 웹사이트에 처음 접속했을 때 세션 토큰 외에 CRSF 공격을 방어하기 위한 토큰을 추가로 클라이언트에게 발급하여, 클라이언트가 웹서버로 어떤 요청을 보낼 때 반드시 거기에 anti-CSRF 토큰을 함께 전송하도록 해서, 그러지 않았다면 그 요청을 무시하도록 하는 방법이 있다. 이렇게 하면 공격자의 웹사이트는 위 공격 외에 피해자가 anti-CSRF 토큰을 발급받도록 하고 또 이를 탈취하는 추가적 공격 로직이 있어야 하는 등의 문제가 발생해 공격이 어려워진다. (참고로, anti-CSRF 토큰으로 CSRF 공격에 조치한 경우에도 XSS 등의 보안 위험이 있다면 이 조치는 뚫릴 위험이 있으므로 CSP 등 이런 부분에 대한 대비가 필요하다.)

    • X-CSRF-Token: anti-CSRF 토큰의 전송 시 보통 HTTP 메시지의 헤더에 이러한 이름을 가진 field의 값으로서 전송되는 경우가 많다.
  • SameSite: CSRF 공격은 ‘의심스런 사이트에서조차 그 웹사이트에 요청을 보낼 때 쿠키가 같이 보내진다’라는 점을 이용한 것이므로, 특정 웹사이트에 요청을 보낼 때 쿠키가 같이 보내질 조건으로서 ‘이 쿠키는 반드시 그 요청이 그 웹사이트에서 발생한 것일 때만 함께 보내진다’라는 속성이 세션 토큰 쿠키에 부여될 수 있다면 CSRF 공격을 원천적으로 차단할 수 있을 것이다. 이에 따라 쿠키에 이와 같은 기능을 지원하는 SameSite 속성이 2016년 Google Chrome 브라우저에서는 51 버전부터 도입되었으며, 이후 다른 브라우저에서도 이 기능을 지원하기 시작했다. (Chrome 브라우저에서는 2020년 2월 발표된 80 버전부터는 서버가 SameSite 속성을 발급하지 않았어도 기본적으로 Lax로 세팅된다.) 단, 이 기능은 도입된 지 얼마 되지 않아 아직 RFC 표준으로 제정된 것은 아니다.

5. JWT(JSON Web Token)

- 쿠키는 웹서버가 사용자를 인증하여 개인화되고 stateful한 통신을 구현하는 데 도움이 되는 중요한 기술이지만, SOP 보안정책이 있어 A 웹사이트에서 얻어온 쿠키를 B 웹사이트에서 스크립트를 통해 이용하는 것이 불가능하다거나 CSRF 공격에 취약하다거나 하는 단점이 있다. 또 기본적으로 stateful한 통신을 구현하기 위한 기술이기 때문에, stateless한 통신에서의 인증이 필요한 상황에서 쓰는 데는 여러 모로 불편한 점이 있다. 이 때문에 JWT라는 새로운 기술이 도입되었으며, 2012년에 RFC 7519로 표준화된 후 현재 이 기술이 필요한 분야에서 널리 쓰이고 있다.

- 간단히 말해, JWT는 다음 과정을 통해 클라이언트의 웹 서버에 대한 사용자 인증에 사용된다.

(1) 클라이언트가 A 웹사이트에 사용자 인증을 받는다. 이때 그 사용자가 인증되면 A 웹사이트는 JWT를 발급하며, 이를 수신한 클라이언트의 브라우저는 브라우저 내 localStorage 등 저장공간에 이를 저장한다.

(2) 클라이언트가 A 웹사이트(또는 거기서 발급받은 JWT를 필요로 하는 B 웹사이트)에 접속할 때, 그 웹사이트는 클라이언트의 브라우저에게 JWT를 요청한다. 이때 일반적으로 JWT는 HTTP 요청에 함께 포함되어 서버로 전송되며, 서버에서는 이를 해독하여 그 값이 서버에서 요구하는 값에 해당하면 그 클라이언트에게 적절한 응답을 리턴한다.

- JWT는 엄밀히 말해 ‘서버로부터 발급받은 암호 문자열’로서, 클라이언트가 서버에 이와 함께 요청 메시지를 전달할 때 JWT를 어떤 방식으로 전송할지는 웹페이지 구현 시 자유롭게 결정할 수 있다. stateless하다는 특징 때문에 RESTful API의 사용자 인증 시 특히 많이 쓰인다.