이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › 보안 › 인증
세션 하이재킹과 세션 고정
세션 ID를 아는 사람이 곧 그 사용자다
서버는 요청의 쿠키에 실려 온 세션 ID만 보고 "누구의 요청인지"를 정합니다. 그래서 로그인된 세션의 ID를 아는 사람은 비밀번호를 몰라도 그 사용자로 행세할 수 있습니다(세션 하이재킹). 세션 고정은 순서를 뒤집은 것입니다. 로그인 전에 쓰던 세션 ID가 로그인 뒤에도 그대로이면, 그 ID를 미리 알고 있던 사람이 로그인된 세션을 쓰게 됩니다.
실행으로 대조 · 스프링 부트 4.1.1과 스프링 시큐리티 7.1.1 서버를 127.0.0.1에 띄워 열두 가지 조합의 로그인 전후 세션 ID와 응답을 대조
이 실습에 나오는 개념
- 세션
- 사용자의 상태를 서버 쪽에 저장하고, 클라이언트에는 그 저장소를 가리키는 식별자(세션 ID)만 쿠키로 전달하는 방식이다. 실제 정보가 서버에 있으므로 클라이언트가 내용을 고칠 수 없다(세션 ID를 빼앗기는 것은 다른 문제다). 서버가 여러 대일 때는 어느 서버로 요청이 가도 같은 세션을 찾을 수 있도록 Redis 같은 공용 저장소에 세션을 두는 구성이 일반적이다.
- 세션 하이재킹
- 다른 사용자의 로그인된 세션 ID를 알아내, 그 ID를 자기 요청의 쿠키에 넣어 그 사용자로 행세하는 것이다. 서버는 쿠키에 실려 온 세션 ID만 보고 누구의 요청인지 정하므로, ID를 아는 사람과 원래 사용자를 구별하지 못한다. 세션 ID는 스크립트가 쿠키를 읽거나, 암호화되지 않은 통신을 누군가 엿볼 때 새어 나간다. 그래서 세션 쿠키에는 HttpOnly와 Secure를 붙이고, 로그아웃할 때 서버의 세션을 지우며, 세션 만료 시간을 길지 않게 둔다.
- 세션 고정(Session Fixation)
- 공격자가 미리 알고 있는 세션 ID를 피해자의 브라우저가 쓰게 만든 뒤, 피해자가 그 세션으로 로그인하기를 기다리는 공격이다. 로그인 전후의 세션 ID가 같으면, 피해자가 로그인하는 순간 공격자가 아는 ID가 로그인된 세션의 ID가 된다. 막는 방법은 로그인에 성공했을 때 서버가 세션 ID를 새 값으로 바꾸는 것이다. 스프링 시큐리티는 따로 설정하지 않아도 로그인할 때 세션 ID를 바꾸며, sessionFixation 설정에서 none을 고르면 이 보호가 꺼진다.
- HttpOnly, Secure, SameSite
- 쿠키를 보호하는 속성들이다. HttpOnly는 자바스크립트가 쿠키를 읽지 못하게 해 스크립트 삽입 공격으로 쿠키가 탈취되는 것을 막는다. Secure는 HTTPS 연결에서만 쿠키를 전송하게 한다. SameSite는 다른 사이트에서 시작된 요청에 쿠키가 함께 전송되는 것을 제한해 CSRF 공격을 줄인다.
- 쿠키
- 서버가 응답에 실어 보내면 브라우저가 저장해 두었다가 같은 사이트에 요청할 때마다 자동으로 함께 보내는 작은 데이터다. HTTP는 요청 사이의 상태를 기억하지 않으므로, 로그인 상태처럼 이어져야 하는 정보를 유지하는 데 쓰인다. 사용자의 브라우저에 저장되므로 내용을 보거나 바꿀 수 있다는 점을 전제로 사용해야 한다.
이렇게 바꿔 보세요
- 처음 설정으로 — 로그인해도 세션 ID가 그대로입니다. 그 ID를 미리 알던 사람의 요청이 kim으로 처리됩니다.
- 스프링 시큐리티의 기본값이면 — 로그인하는 순간 ID가 바뀌어, 미리 알아 둔 ID는 쓸모가 없어집니다.
- 새 세션을 만드는 방식 — ID가 바뀌는 것은 같지만 로그인 전에 담은 장바구니가 사라집니다.
- 로그인 뒤의 ID가 새어 나가면 — 로그인할 때 ID를 바꾸는 것으로는 막지 못합니다.
- 피해자가 로그아웃하면 — 서버가 세션을 지워, 새어 나간 ID가 더는 통하지 않습니다.
- 보호를 껐지만 로그아웃한 경우 — 로그아웃한 뒤에는 막히지만, 로그인해 있는 동안은 열려 있었습니다.
처음 설정으로 실행하면
- 다른 사람(공격자)이 shop.example의 로그인 화면을 엽니다. 서버는 처음 온 브라우저에 세션을 하나 만들고, 응답 헤더 Set-Cookie로 세션 ID aaa111을 내려 줍니다. 아직 아무도 로그인하지 않은 세션입니다. 공격자는 이 ID를 적어 둡니다.
- 공격자가 피해자의 브라우저에 이 세션 ID가 쿠키로 들어가게 만듭니다. 방법은 이 실습에서 다루지 않고, "피해자의 브라우저가 aaa111을 쿠키로 보낸다"는 결과만 가정합니다. 이 시점에는 아직 얻은 것이 없습니다. 로그인하지 않은 세션이기 때문입니다.
- POST /cart: 피해자가 로그인하기 전에 키보드를 장바구니에 담습니다. 브라우저가 쿠키 JSESSIONID=aaa111을 함께 보내고, 서버는 그 세션에 장바구니를 적어 둡니다.
- POST /login: 피해자가 아이디와 비밀번호를 보냅니다. 쿠키는 여전히 JSESSIONID=aaa111입니다. 서버가 아이디와 비밀번호를 확인했고, 인증에 성공했습니다.
- 로그인 성공 직후: 세션에 아무 처리도 하지 않습니다. 로그인한 사용자 kim이 원래 세션 aaa111에 그대로 기록되고, 응답에는 새 Set-Cookie가 없습니다. 로그인 전에 쓰던 ID가 그대로 로그인된 세션의 ID가 됐습니다.
- GET /cart: 피해자의 브라우저는 이제 JSESSIONID=aaa111 쿠키를 보냅니다. 장바구니에 키보드가 남아 있습니다.
- GET /me: 공격자가 자기 컴퓨터에서, 알고 있는 ID를 쿠키(JSESSIONID=aaa111)에 넣어 요청합니다. 서버는 그 ID로 세션을 찾고, 거기 기록된 kim의 요청으로 처리해 200으로 답합니다. 서버가 보는 것은 세션 ID뿐이라 어느 컴퓨터에서 보냈는지는 구별하지 않습니다. 공격자는 비밀번호를 한 번도 입력하지 않았습니다.
실행 결과
- 세션 고정: 로그인 전후의 세션 ID가 같으면, 그 ID를 미리 알고 있던 사람은 비밀번호를 몰라도 로그인된 세션을 쓰게 됩니다. 로그인에 성공하는 순간 세션 ID를 바꾸면 막을 수 있고, 스프링 시큐리티는 따로 설정하지 않아도 그렇게 합니다.
코드
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(a -> a
.requestMatchers("/cart").permitAll() // 장바구니는 로그인 전에도 쓴다
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults()) // POST /login 으로 로그인
.sessionManagement(s -> s
.sessionFixation(f -> f.none())) // 로그인해도 세션을 그대로 둔다 (보호를 끔)
.build();
}
@GetMapping("/me")
String me(Principal p) {
return p.getName(); // 이 요청이 누구의 요청으로 처리됐는지
}
@PostMapping("/cart")
String add(@RequestParam String item, HttpSession session) {
session.setAttribute("cart", item); // 세션에 장바구니를 적어 둔다
return "ok";
}
// 서버가 세션 ID를 내려 줄 때의 응답 헤더 (스프링 부트 기본 설정)
// Set-Cookie: JSESSIONID=aaa111; Path=/; HttpOnly
이 모형의 전제
열두 가지 조합 모두를 이 PC에서 스프링 부트 4.1.1과 스프링 시큐리티 7.1.1 서버를 127.0.0.1에 띄우고 화면과 같은 설정으로 실행해 대조했습니다. 폼 로그인 전후에 서버가 내려 준 JSESSIONID가 바뀌는지, 로그인 전에 담은 장바구니가 남는지, 다른 사람이 알고 있는 ID를 쿠키에 넣어 보낸 요청의 응답(200과 본문 kim, 또는 302와 /login)이 화면과 같은지 확인했습니다. 여기서 "공격자"와 "피해자"는 쿠키에 넣는 세션 ID만 다른 같은 시험 프로그램의 요청이며, 실제 브라우저는 쓰지 않았습니다. 공격자가 피해자의 브라우저에 세션 ID를 심는 방법과 로그인된 세션의 ID를 알아내는 방법은 실행하지 않았고, 그렇게 됐다는 결과만 가정했습니다. HttpOnly 쿠키를 스크립트가 읽지 못한다는 것은 "쿠키" 실습에서 실제 브라우저로 확인한 내용입니다. 실제 세션 ID는 32글자였고(이 서버에서 확인) 추측할 수 없도록 무작위로 만들어집니다. 화면의 aaa111과 bbb222는 알아보기 쉽게 줄여 적은 것입니다. 서버가 세션 ID를 내려 줄 때의 Set-Cookie 속성이 "Path=/; HttpOnly"라는 것, server.servlet.session.cookie.secure=true로 설정하면 Secure가 붙는다는 것도 이 서버에서 확인했습니다. 로그인할 때의 동작을 changeSessionId나 migrateSession으로 명시해도 밖에서 보이는 결과는 기본값과 같았습니다. 로그인 화면을 처음 열 때 세션이 만들어지는 것은 그 화면에 넣는 CSRF 토큰을 세션에 보관하기 때문입니다. 세션 만료 시간이 지나 세션이 없어지는 것은 실행으로 확인하지 않았습니다. 근거 문서는 스프링 시큐리티 문서의 Servlet Applications, Authentication, Session Management 장에 있는 세션 고정 보호 설명입니다. 서버가 여러 대일 때의 세션 공유는 "세션과 JWT" 실습에서 다룹니다.
앞 실습: OAuth 로그인 흐름 · 다음 실습: 시큐리티 필터 체인