728x90
안녕하세요! 프론트엔드/풀스택 실무에서 자주 부딪히는 “JWT를 어디에 보관할 것인가” 문제를 정리했습니다. 저장 위치별 보안/UX 트레이드오프, 실제로 일어나는 탈취(steal) 시나리오, 그리고 탈취 되었을 때 어떻게 눈치채고 대응할지까지 바로 적용 가능한 체크리스트로 담았습니다.
저장 위치별 비교
구분 | localStorage | sessionStorage | Cookie (HttpOnly 권장) |
---|---|---|---|
접근성 | JS로 window.localStorage 읽기/쓰기 |
탭 수명(탭 닫히면 소멸) | JS 접근 불가(HttpOnly)·자동 전송(도메인/경로 일치 시) |
지속성 | 브라우저 종료 후에도 유지 | 탭 살아있는 동안만 | 만료 시각/세션/영구 선택 가능 |
XSS에 대한 노출 | 높음(스크립트가 읽을 수 있음) | 높음 | 낮음(HttpOnly면 읽기 불가) |
CSRF 위험 | 낮음(자동 전송 안 함) | 낮음 | 높음(자동 전송) → SameSite/CSRF 토큰 필요 |
도메인/경로 스코프 | 없음(도메인 전역에서 JS로 접근) | 없음 | 세밀하게 설정 가능(domain/path) |
구현 난이도 | 쉬움 | 쉬움 | 쿠키 옵션/CSRF 방어 설계 필요 |
요약 권장 패턴(현업 다수 채택)
- Access Token: 메모리에만 보관(새로고침 때 사라져도 OK)
- Refresh Token: HttpOnly + Secure + SameSite 쿠키로 저장 + Refresh Token Rotation
- 이유: XSS로부터 Access Token 노출을 줄이고, CSRF는 쿠키 보안옵션과 토큰 방식으로 방어
- 참고: local/sessionStorage를 꼭 써야 한다면, 짧은 만료 + 철저한 XSS 방어(CSP/Trusted Types)를 전제로만 고려
대표적 탈취 공격 시나리오
A. XSS(교차 사이트 스크립팅)
- 대상: localStorage / sessionStorage / 비-HttpOnly 쿠키
- 방법: 취약 페이지에 삽입된 스크립트가
localStorage.getItem('token')
등으로 탈취 - 완화: 입력 검증/인코딩, CSP(스크립트 소스 화이트리스트), Trusted Types, 라이브러리 취약점 패치, HttpOnly 쿠키 사용
B. CSRF(사이트 간 요청 위조)
- 대상: 쿠키 기반 인증(자동 전송)
- 방법: 피해자 브라우저가 공격자 페이지에서 의도치 않은 인증 요청을 보냄
- 완화: SameSite=Lax/Strict, CSRF 토큰(Double Submit / Synchronizer Token), 쿠키에 SameSite·Secure·HttpOnly, 쿠키 경로/도메인 최소화
C. 전송 구간 탈취(MITM)
- 대상: 모든 저장 방식 (전송 시)
- 방법: HTTPS 미사용, 중간자 공격으로 Authorization 헤더/쿠키 가로채기
- 완화: HTTPS 강제(HSTS), TLS 최신 설정, 공용망 환경 주의
D. Refresh Token 재사용/재발급 악용
- 대상: 장기 수명 Refresh Token
- 방법: 한 번 유출되면 지속 액세스 토큰 발급 남용
- 완화: Refresh Token Rotation(매번 교체 + 재사용 감지 시 즉시 무효), 짧은 수명 + 기기 바인딩(JTI/디바이스 ID), IP/디바이스 변동 감시
E. 로그·리퍼러·서드파티 유출
- 대상: 토큰을 URL 쿼리/프래그먼트로 주고받을 때
- 방법: 서버/클라이언트 로그에 남거나 Referer 헤더로 외부 도메인에 유출
- 완화: 토큰을 URL에 절대 넣지 않기, 민감 값은 헤더/바디로만, 로깅 필터링
F. 브라우저 확장 프로그램/악성 SW
- 대상: local/sessionStorage·비-HttpOnly 쿠키
- 방법: 권한 과도한 확장/악성코드가 DOM/Storage 읽기
- 완화: HttpOnly 쿠키, 보안 교육/정책, EDR/안티바이러스
G. 클릭재킹/XS-Leaks(간접 유출)
- 대상: 쿠키 세션/특정 뷰
- 완화: X-Frame-Options/frame-ancestors(CSP), 숨김 필드/타이밍·상태 유출 방어
탈취되었는지 어떻게 알까? (실무 탐지 시그널)
서버·백엔드 관점의 행동 기반 탐지가 핵심입니다.
액세스/리프레시 토큰 레벨
- Refresh Token Rotation 재사용 감지: RT는 매 재발급마다 새 토큰으로 교체. 이전 RT가 다시 쓰이면 탈취로 간주 → 전체 세션 무효화 + 계정 보호 플로우 발동
- JTI(토큰 고유 ID)·세션 테이블: 각 RT/세션에 고유 식별자 저장, 만료 전이라도 서버측 블랙리스트/리보크 가능
세션/행동 이상치
- 불가능한 이동(Impossible Travel): 짧은 시간 내 물리적으로 불가능한 Geo-IP 이동, 이례적 ASN·프록시 사용
- 디바이스/브라우저 지문 급변: User-Agent, 클라이언트 힌트, 해시된 지문 값 급변
- 동시 사용 패턴: 동일 계정이 다중 지역/기기에서 동시에 액세스 토큰 재발급 시도
- 이상 트래픽/레이트 초과: 비정상적인 로그인 시도, 재발급 엔드포인트로의 급증
- 서명/클레임 무결성 실패: 서명 검증 실패, iss/aud/exp/nbf 등 클레임 이상치
클라이언트/프론트 관점(보조적)
- “기기 로그아웃됨” 빈번, 사용자 알림/2FA 푸시가 계속 뜸
- 저장소에 있던 토큰이 예상치 못하게 무효화(서버가 강제 리보크했을 가능성)
안전한 설계 패턴(권장 레시피)
패턴 A: “메모리 AT + HttpOnly RT”
- Access Token(짧은 수명): 앱 메모리만 저장
- Refresh Token(길게, Rotation): HttpOnly + Secure + SameSite=Lax/Strict 쿠키
- 이점: XSS로 AT 직접 유출 어려움, CSRF는 쿠키 보안옵션+CSRF 토큰으로 방어
- 주의: 새로고침 시 AT 사라지므로 Silent Refresh 흐름 구현 필요
패턴 B: 전부 쿠키 기반(AT/RT 둘 다 쿠키, AT는 짧게)
- 전달: 쿠키 자동 전송
- 필수: SameSite + CSRF 토큰(폼/헤더-기반 대조), 경로/도메인 최소화
- 장점: SSR/MPA 친화
- 단점: CSRF 설계 복잡도↑
(선택) 쿠키 하드닝 팁
- HttpOnly, Secure, SameSite=Lax/Strict
- __Host- 프리픽스(도메인 지정 금지+Secure+Path=/ 조합)
- 민감 엔드포인트는 CORS 엄격·프리플라이트 필수
실전 방어 체크리스트
앱/프론트
- 토큰을 URL에 절대 담지 않기
- CSP(스크립트 소스 화이트리스트) + Trusted Types
- 라이브러리/빌드 체인 취약점 정기 패치
- 서드파티 스크립트·확장 권한 최소화
쿠키/세션
- HttpOnly / Secure / SameSite 필수
- __Host- 프리픽스 고려, 도메인/경로 최소 권한
- CSRF 토큰(Double Submit / 상태값-검증형) + Referrer/Origin 검증
토큰 수명/회전
- AT 매우 짧게(분 단위), RT는 Rotation + 재사용 탐지
- jti/세션 테이블로 서버측 리보크 가능하게
- 고위험 액션은 재인증/2FA 요구
네트워크/플랫폼
- HTTPS 강제(HSTS), 최신 TLS
- 레이트리밋·IP/ASN 평판 기반 차단
- 보안 로깅(개인정보 마스킹), SIEM 연동
탈취 의심 시 “대응 프로토콜”
즉시 리스크 차단
- 해당 계정의 모든 Refresh Token 무효화(세션 테이블/블랙리스트)
- 재사용된 RT 발견 시 계정 보호 모드(강제 로그아웃, 비밀번호/2FA 재설정)
조사 & 범위 파악
- 로그로 유출 시점·경로 추정(XSS, URL 누출, RT 재사용 IP 등)
- 동일 패턴 다른 계정 영향 검사
취약점 핫픽스 + 재발 방지
- CSP/TT 강화, 취약 입력 지점 패치, 토큰 수명·회전 정책 재점검
사용자 커뮤니케이션
- 안전한 공지·가이드(의심 로그인 내역, 재인증 안내)
코드 스니펫 모음
(서버) 쿠키 설정 예시 (Node/Express)
res.cookie('__Host-rt', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'lax', // 민감도에 따라 'strict' 고려
path: '/', // __Host- 프리픽스는 path=/ 필수
maxAge: 1000 * 60 * 60 * 24 * 7, // 7일 등
});
(서버) CSRF Double-Submit 패턴 핵심
// 1) 로그인/초기 로드 시 CSRF 토큰 발급(쿠키와 응답 바디/헤더에 동시에)
res.cookie('csrf', csrfToken, { httpOnly: false, sameSite: 'lax', secure: true });
// 2) 클라이언트는 민감요청에 헤더로 전송
fetch('/api/transfer', {
method: 'POST',
headers: { 'X-CSRF-Token': readFromCookie('csrf') },
body: JSON.stringify(payload),
});
// 3) 서버에서 쿠키값과 헤더값 일치 확인 + Origin/Referer 체크
(서버) Refresh Token Rotation 감지(개념)
// RT 테이블: { userId, tokenHash, revoked:boolean, createdAt, replacedById, lastUsedAt }
function rotateRefreshToken(oldRtId, userId) {
// 1) oldRtId 사용해 새 RT 발급 + oldRt.replacedById = newRtId
// 2) 요청 시 전달된 RT가 이미 replacedById를 가진 토큰이면 -> 재사용 탐지
// => 계정 보호 모드 + 전체 세션 리보크
}
결론: “완벽한 곳은 없다, 조합이 답”
XSS vs CSRF는 보안 축이 다릅니다. 대다수 SPA/SSR 서비스는 “AT는 메모리”, “RT는 HttpOnly 쿠키 + Rotation”으로 XSS·CSRF를 동시에 관리합니다. 여기에 짧은 토큰 수명, CSP/Trusted Types, SIEM 기반 이상행동 탐지를 결합할 때 탈취 확률은 낮아지고, 탈취 시에도 빠르게 감지·차단할 수 있습니다.
핵심 요약
- 보관 위치 결론: AT는 메모리, RT는 HttpOnly 쿠키(+Rotation)
- XSS vs CSRF: 서로 다른 축이므로 함께 고려
- 핵심 방어: SameSite/CSRF 토큰, 짧은 수명, Rotation, CSP/TT, HSTS/TLS
- 탐지 시그널: RT 재사용, 불가능한 이동, 지문 급변, 레이트 초과
- 대응 프로토콜: 전 계정 RT 무효화 → 조사 → 핫픽스 → 사용자 안내
728x90
'Frontend Development' 카테고리의 다른 글
React 리렌더링(Re-rendering): Trigger → Render → Commit (0) | 2025.10.03 |
---|---|
Next.js SSR 페이지 풀 페이지 캐싱 (0) | 2025.09.08 |
useEffect에서 setInterval이 상태를 못 따라오는 이유 (stale closure) (0) | 2025.09.07 |
React Error Boundary: 왜 아직도 클래스일까? (0) | 2025.09.07 |
setTimeout vs Promise.then vs queueMicrotask (0) | 2025.09.05 |