Cookie 자동 전송과 CSRF: SameSite·CSRF 토큰·일렉트론까지 정리
왜 JWT는 cookie vs localStorage 토론에서 의견이 갈리는가. Cookie의 자동 전송이라는 강점이 동시에 CSRF 취약점이 되는 메커니즘, SameSite·CSRF 토큰·Double Submit 방어법, 그리고 일렉트론 환경에서는 왜 cookie가 도리어 부적합한지까지 정리했다.
Cookie 자동 전송과 CSRF 정리
"JWT는 cookie가 안전한가, localStorage가 안전한가"는 표면적으로 보안 토론이지만, 답은 각 저장소가 어떤 위협 모델에 강하고 어떤 위협 모델에 약한가의 비교다. 이 글에서 그 분기 지점을 풀어본다.
1. Cookie의 "자동 전송"이 뭔가
브라우저는 cookie에 매우 특별한 규칙을 갖고 있다.
같은 origin(혹은 cookie의 domain 속성에 매칭되는 곳)으로 HTTP 요청이 나갈 때, 브라우저가 알아서 cookie를 헤더에 붙여서 보낸다.
흐름은 이렇다.
- 사용자가
example.com에 로그인 - 서버가 응답에
Set-Cookie: sessionId=ABC; HttpOnly - 이후
example.com으로 가는 모든 요청 → 자동으로Cookie: sessionId=ABC헤더 첨부 - JavaScript가
fetch('/api/...')호출하든,<img src="...">든,<a href>로 이동하든 전부
이게 왜 강력한가.
- JS 코드가 토큰을 만질 필요가 없음 —
Authorization헤더 직접 붙이는 코드 안 짜도 됨 - HttpOnly 플래그를 켜면 JS에서
document.cookie로도 못 읽음 → XSS 공격이 토큰을 탈취 못 함 - Secure 플래그로 HTTPS에서만 전송, SameSite로 외부 사이트에서의 자동 전송을 차단
- 서버 입장에선 매 요청마다 세션을 식별 가능
Cookie vs Authorization 헤더 비교
| Cookie (HttpOnly) | Authorization 헤더 | |
|---|---|---|
| 전송 주체 | 브라우저 자동 | JS 코드가 명시적으로 |
| XSS로 토큰 탈취 | 불가 (HttpOnly) | 가능 (localStorage/메모리에 있으니 JS가 읽음) |
| 외부 사이트가 악용 가능? | 가능 (자동 전송이라) → CSRF 위협 | 불가 (외부 JS는 헤더 못 붙임) |
| 모바일/CLI/다른 origin 클라 | 까다로움 | 쉬움 |
표만 봐도 양쪽이 정확히 반대 방향의 강점을 갖는다. 한쪽이 안전한 곳에서 다른 쪽이 취약하고, 그 반대도 마찬가지다.
2. CSRF: Cookie의 자동 전송이 곧 취약점
Cookie의 자동 전송은 편리함이자 동시에 CSRF (Cross-Site Request Forgery) 의 근원이다.
공격 시나리오
- 사용자가
bank.com에 로그인. 브라우저에 cookiesessionId=ABC보관됨 - 사용자가 같은 브라우저로
evil.com방문 (XSS도 아님, 그냥 평범한 사이트) evil.com의 HTML 안에 이런 게 숨어있음:
<form action="https://bank.com/transfer" method="POST" id="f">
<input name="to" value="attacker">
<input name="amount" value="10000000">
</form>
<script>document.getElementById('f').submit()</script>- 폼이 자동 submit → 브라우저가
bank.com으로 POST 요청 보냄 → cookie도 자동 첨부됨 →bank.com서버는 "이건 로그인된 사용자의 정상 요청이네" 판단 → 송금 실행
핵심: 공격자는 사용자의 cookie 값을 읽지 못한다. 그런데 사용자의 브라우저가 알아서 보내준다는 점을 악용한다. 사용자는 자기가 송금당했는지 모른다.
왜 Authorization 헤더는 안전한가
evil.com의 JavaScript는 bank.com으로 fetch를 보낼 수 있지만, Authorization 헤더는 직접 붙여야 한다. 그런데 그 토큰은 bank.com의 메모리/localStorage에 있고, same-origin policy 때문에 evil.com의 JS는 bank.com의 저장소를 못 읽는다. 결국 헤더에 토큰을 붙일 방법이 없어서 공격이 불가능하다.
3. CSRF 방어 4가지
Cookie 방식을 쓸 거면 CSRF 방어가 필수다. 현대 표준에 가까운 순서로 4가지를 정리한다.
(a) SameSite cookie 속성: 현대 표준, 가장 중요
Set-Cookie: sessionId=ABC; HttpOnly; Secure; SameSite=LaxSameSite=Strict: 다른 사이트에서 출발한 요청엔 cookie 절대 안 보냄. 가장 강력하지만, 이메일 링크 타고 들어왔을 때도 로그인 풀린 것처럼 보여서 UX가 나쁨SameSite=Lax(현대 브라우저 기본값): top-level navigation(주소창 입력, 링크 클릭)엔 보내지만, iframe/POST/이미지 임베드 같은 cross-site 요청엔 안 보냄. CSRF 대부분이 이걸로 막힌다SameSite=None; Secure: 모든 cross-site에 보냄 (구식 방식, OAuth 콜백 등 특수 케이스만)
대부분의 현대 앱은 SameSite=Lax로 시작해서, 민감한 작업만 추가 방어(다음 b·c)를 더하는 패턴이다.
(b) CSRF 토큰: Synchronizer Token Pattern
서버가 페이지 렌더링 시 랜덤 토큰을 생성한다.
- HTML에 hidden input으로 박음 (
<input name="_csrf" value="xyz123">) - 동시에 cookie에도 같은 값 저장 (또는 서버 세션에)
폼 제출 시 hidden input 값과 cookie 값을 비교 → 다르면 거부.
evil.com은 페이지의 hidden 토큰을 읽을 방법이 없어서(same-origin policy) 공격이 불가능하다. Spring Security, Django, Rails 모두 기본 탑재된 메커니즘이다.
(c) Double Submit Cookie
서버가 cookie와 헤더 양쪽에 같은 토큰을 심으라고 요구한다. JS가 cookie 값을 읽어서 X-CSRF-Token 헤더에 넣어 전송한다. 서버는 둘이 같은지 비교한다.
evil.com은 사용자의 cookie 값을 읽지 못하니까 헤더에 정확한 값을 못 넣음 → 차단
이 방식의 강점은 서버가 세션 저장소를 따로 안 가져도 된다는 점이다(stateless). 그래서 마이크로서비스나 서버리스 환경에서 자주 쓰인다.
(d) Origin / Referer 헤더 검증
요청이 어디서 출발했는지 Origin 헤더로 확인한다. bank.com만 허용.
보조 수단으로 좋지만 단독으로는 부족하다(헤더 누락 케이스 존재). a·b·c와 함께 쓰는 게 정석이다.
4. 위협 모델 비교: 어떤 공격에 누가 강한가
| 공격 | Cookie (HttpOnly + SameSite=Lax) | localStorage / 메모리 + Bearer |
|---|---|---|
| XSS로 토큰 탈취 | ✅ 안전 (JS가 못 읽음) | ❌ 취약 (JS가 읽을 수 있음) |
| CSRF (다른 사이트 요청) | ⚠️ SameSite로 거의 막힘 + 토큰 추가 가능 | ✅ 안전 (헤더를 못 붙임) |
| MITM (중간자 공격) | Secure로 막힘 | HTTPS면 막힘 |
| 공용 PC에서 다음 사용자가 본다 | 브라우저 종료 시 sessionStorage처럼 만들 수 있음 | localStorage는 영구. 메모리는 종료 시 사라짐 |
결론: 어느 한쪽이 우월하지 않다. 위협 모델에 따라 답이 달라진다.
- 우리 앱에서 XSS 가능성이 두려우면 → HttpOnly Cookie (+ SameSite + CSRF 토큰)
- 우리 앱이 모바일/데스크톱 클라도 같이 봐야 하면 → Bearer 토큰 (Authorization 헤더)
- 둘 다 → 보통 모바일은 Bearer, 웹은 Cookie의 하이브리드
5. 일렉트론에서는 cookie가 도리어 부적합한 이유
흔히 "데스크톱 앱이니까 cookie도 그대로 쓰면 되겠지"라고 생각하기 쉬운데, 일렉트론에서는 cookie의 강점이 거의 다 사라진다.
일렉트론에서 cookie가 까다로운 4가지 이유
- Origin 개념이 모호: 웹은
app.example.com도메인에 cookie가 묶이지만, 일렉트론은file://또는app://같은 로컬 스킴이라 백엔드(https://api.example.com)와 다른 origin. SameSite/CrossOrigin 정책에 걸려서 cookie가 자동으로 전송되지 않을 수 있음 document.cookie로 쓴 값은 휘발성에 가까움: 메인 프로세스가 종료되면 사라지거나(세션 쿠키), 영구 저장 설정(session.setStorageAccessHandler,partition: "persist:...") 명시 안 하면 재시작 시 날아감max-age가 무의미해질 수 있음: 앱이 백그라운드로 들어갔다 며칠 뒤 깨어나면 시계 기준이 달라져서 만료 판정이 어긋날 수 있음- CSRF 보호의 의미가 사라짐: cookie의 강점이 "서버가 클라 도움 없이 자동 전송"인데, 일렉트론은 자체 앱이라 CSRF 위협 모델 자체가 없어서 cookie의 보안 이점이 희석됨
일렉트론에 더 맞는 패턴
| 저장소 | 특성 | 이 앱에 어울림 |
|---|---|---|
electron-store (디스크 JSON) | 영구, 평문 | 빠른 시작 — 보안 낮음 |
OS keychain (keytar / safeStorage) | 영구, OS 암호화 | 권장 — 토큰엔 이게 정석 |
localStorage | 영구, 평문, 렌더러만 | 임시용 |
| Cookie | 가능하지만 origin 이슈 | 비추 |
구체 권장안
- refreshToken: 메인 프로세스가
safeStorage(Electron 13+) 또는keytar로 OS 키체인에 저장. 렌더러는 IPC로 요청해서 받음 (직접 접근 불가) - accessToken: 메모리(렌더러 변수)에만. 새로고침/재시작 시 refreshToken으로 즉시 갱신
- Authorization 헤더로 전송: cookie 자동 전송 의존 안 하고, fetch에 명시적으로
Authorization: Bearer ...붙임 - 백엔드는 변경 거의 없음: 어차피 Bearer 토큰 검증이라 origin/cookie와 무관
6. 우리 앱이라면 어떻게 결정하나: 체크리스트
위 내용을 정리해서, 실제 결정 시 따라가는 체크리스트다.
1) 클라이언트 환경은?
- 웹 only → cookie 또는 localStorage 둘 다 가능
- 웹 + 모바일/CLI → Bearer 토큰 (cookie는 모바일에서 까다로움)
- 일렉트론/데스크톱 → OS keychain + Bearer (cookie는 도리어 부적합)
2) Next.js 미들웨어를 쓰는가?
- 쓴다 → cookie 필수 (미들웨어는 Edge Runtime이라 localStorage 못 봄)
- 안 쓴다 → 자유로움
3) XSS와 CSRF 중 어느 쪽이 더 두려운가?
- XSS 두려움 > CSRF → cookie (HttpOnly로 토큰 탈취 차단)
- CSRF 두려움 > XSS → Bearer (외부 사이트가 헤더 못 붙임)
- 보통 답은 "둘 다 두렵다, 하이브리드로 가자"
4) cookie를 쓴다면 다음 4가지를 반드시
HttpOnly— JS 읽기 차단Secure— HTTPS 강제SameSite=Lax또는Strict- (민감 액션) CSRF 토큰 또는 Double Submit
마치며
JWT 저장 위치 토론은 일반적으로 "어느 쪽이 더 안전한가"로 흐르지만, 답은 "내 환경에서 어떤 위협이 더 현실적인가" 다.
- 웹 only + XSS 두려움 큼 → HttpOnly Cookie + SameSite + CSRF 토큰
- 모바일 클라 포함 → Bearer 헤더
- Next.js 미들웨어 사용 → Cookie (선택지 없음)
- 일렉트론 → OS keychain + Bearer (Cookie는 도리어 부적합)
이 4가지 case가 다른 답을 내놓는다는 사실 자체가 정답이 하나가 아니라는 증거다. 추상적인 보안 점수표보다 자기 앱의 실제 위협 모델을 먼저 그리는 게 결정의 정확도를 가장 크게 높인다.