
XSS(Cross-Site Scripting)와 CSRF(Cross-Site Request Forgery) 공격 모두 SOP(Same Origin Policy) 정책으로 방어할 수 없는 공격이다.
SOP에 대한 개념이 혼동되면 아래 CORS 설명글의 SOP 파트만 읽어보는 것을 추천한다. (이번 글의 내용이 CORS와 관계가 있는 것은 아니다.)
이전 글에서 설명된 공격 시나리오는, 공격자가 사용자의 특정 웹사이트 쿠키를 훔쳐 해당 웹사이트의 권한을 도용하려는 상황이었다. 하지만 SOP로 인해 공격자가 직접 출처가 다른 웹사이트의 쿠키를 훔치는 것은 불가능하다.
예를 들어, 공격자의 attacker.com이 사용자의 gmail.com에 대한 쿠키를 훔치려고 시도한다고 해보자. (사용자는 이미 gmail.com에 로그인 성공 후 쿠키를 발급받은 상황이다.)
attacker.com은 사용자를 해당 사이트로 불러들인 후, 악성 JavaScript를 통해 현재 사용자의 gmail.com 쿠키를 읽으려고 한다.
하지만 attacker.com은 SOP로 인해 gmail.com의 쿠키에 접근할 수 없고, 쿠키에 접근할 수 있는 것은 쿠키를 발급한 gmail.com과 같은 출처의 웹사이트 뿐이다.
공격자는 생각한다. gmail.com만이 쿠키에 접근할 수 있다면, gmail이 사용자 쿠키에 접근하여 값을 읽은 후, 그 값을 자신(공격자)에게 보내도록 조종하면 되지 않을까? 이것이 이번 글에서 설명할 공격의 배경이다.
이번 글에서는 각 공격의 시나리오와, 해결책에 대해서 알아보자.
XSS(Cross-Site Scripting)는 공격자가 웹사이트에 악성 스크립트를 삽입하는 공격이다.
SOP로 인해 브라우저는 다른 출처(도메인, 프로토콜, 포트)에서 온 리소스 간의 상호작용을 제한하지만, XSS 공격 스크립트는 현재 웹사이트 환경에서 실행되는 것이므로 리소스에 접근할 수 있다. 즉, 사용자의 쿠키를 읽을 수 있다.
XSS는 공격 방식과 악성 스크립트의 위치에 따라 Stored XSS, Reflected XSS, DOM-Based XSS 세 가지로 구분된다.

Stored XSS는 말 그대로 웹 애플리케이션의 저장된 데이터(데이터베이스)에 악성 스크립트 코드를 삽입하는 공격이다.
다른 사용자가 악성 스크립트 코드가 삽입된 웹사이트 부분에 접근하면, 악성 스크립트가 콘텐츠의 일부로 제공되며, 사용자의 브라우저에서 실행되게 된다. 보통 게시글, 댓글과 같은 형태로 공격이 수행되며, 공격 과정은 아래와 같다.
<script>...</script> 스크립트가 담긴 댓글을 등록한다.해당 스크립트는 목표 웹사이트의 도메인 네임으로 실행되기에, SOP 제한 대상이 아니다.
(스크립트가 쿠키를 읽은 후 공격자에게 전송하는 스크립트였다고 생각해보자.)

Reflected XSS는 파라미터를 통한 요청의 취약점을 이용하는 공격이다.
다시 설명하면, 사용자가 제공한 입력값이 서버에서 처리된 후, 반환값이 웹사이트 페이지에 반영(Reflect)되는 것을 이용한 공격이다. 이는 주로 검색 결과, 에러 메시지 등을 표시할 때 파라미터를 통한 요청이 수행되며 발생한다.
예를 들어, http://example.com/search?keyword=<script>...<script>와 같이 검색 URL의 쿼리 스트링에 공격 스크립트를 넣을 수 있다. 웹사이트가 필터링 없이 검색 결과를 페이지에 표시하는 경우, 해당 스크립트가 실행된다.
Reflected XSS는 URL 요청의 일부를 페이지에 담아내는 취약점이 있는 웹사이트에 쉽게 가해질 수 있는 공격이다.

DOM-Based XSS는 클라이언트 측의 DOM(Document Object Model)에서 발생하는 XSS 공격으로, 브라우저에서 직접적으로 DOM을 조작함으로써 이루어진다.
📌 DOM (Document Object Model)
DOM은 XML, HTML 문서의 각 항목을 계층으로 표현하여 생성, 변형, 삭제할 수 있도록 돕는 인터페이스이다.
직관적으로 설명하면, HTML의 문법이 태그의 집합(
body,div,h1...)으로 구성되어 있을 때, 이러한 태그들이 이루는 계층 구조를 DOM이라고 한다.
DOM-Based XSS는 브라우저가 DOM을 동적으로 수정(요소 추가, 삭제 등)할 때 접근하는 JavaScript에 악성 스크립트를 삽입하는 공격이다.
앞서 설명한 Stored XSS와 Reflected XSS 모두 서버 측 취약점을 활용한 공격으로, 서버 측 응답 페이지에 악성 스크립트가 포함되어 사용자 브라우저가 해당 스크립트를 실행하도록 한다. 사실 웹 방화벽은 응답 페이지에 악성 스크립트가 포함된 경우 이를 감지하고 차단하기에, 해당 공격들은 쉽게 차단될 수 있다.
DOM-Based XSS의 경우, 서버와 무관한 클라이언트 측 취약점을 활용한 공격이다. 즉, 서버 측 응답 페이지에 스크립트가 포함되어 있지 않더라도, 브라우저가 해당 페이지의 HTML을 동적으로 업데이트하면서 발생한다.
따라서 XSS 공격에 대한 대처 방안으로 웹 방화벽에 의존하는 경우, URL 끝에 #을 적은 후 악성 스크립트를 위치시키면 브라우저는 # 뒤의 내용을 서버에 전달하지 않기 때문에, DOM-Based XSS 공격에 취약해질 수 있다.
DOM-Based XSS 공격을 방어하기 위해, 대표적으로는 입력 값을 검증하여 HTML 특수 문자를 이스케이프 처리하는 방법을 사용할 수 있다.
📌 DOM-Based XSS 추가 설명
"You searched for 검색어" 형태로 사용자가 입력한 검색어를 화면에 표시해주는 웹사이트를 본 적이 있을 것이다. 페이지에서 해당 문장을 표시할 때 "검색어" 부분이 자동으로 바뀌는 것은 서버가 아닌, 클라이언트 측에서 사용자 입력 값을 통해 동적 페이지를 구성하는 것이다. 이것이 DOM-Based XSS 공격에 취약한 예시 상황이라고 할 수 있다.
CSRF(Cross-Site Request Forgery) 공격은 사용자의 권한을 이용해 원하지 않는 작업을 수행하도록 유도하는 공격 기법이다. 공격자는 피해자가 로그인한 상태에서 악성 웹사이트를 방문하게 하여, 피해자의 브라우저를 통해 요청을 전송하도록 한다.
Cross-Site Request란 현재 사이트가 아닌 다른 사이트에서 리소스를 받아 온다는 의미라는 것이므로, 이러한 행위에 대해 사기치겠다! 라는 것이 CSRF의 컨셉이라고 할 수 있겠다.
따라서 CSRF는 XSS와는 다른 방식으로 SOP를 우회한다는 것을 알 수 있다. XSS는 현재 웹사이트 환경에서 리소스에 접근을 시도하고, 결과적으로는 사용자의 쿠키를 취득하는 것이 최종 목적이라고 할 수 있다. 반면 CSRF의 경우 이미 로그인 된 사용자의 권한을 이용해서 원하는 작업을 수행하도록 하는 공격으로, 쿠키를 훔치는 것이 목적이 아니다.

그림에서 설명하는 과정은 위와 같다. 보다 정확하고 구체적인 설명을 위해 가정을 하나 추가해보자.
그렇다면 4번 과정에서 공격자는 어떤 작업을 수행할 수 있을까? 맞다! 공격자 웹사이트에 "OO은행에 접속 후 공격자 계좌로 100만원을 이체해라" 라는 payload가 작성되어 있는 경우, 사용자가 공격자 웹사이트를 방문하는 순간 해당 작업이 그대로 진행된다. 왜? 사용자는 이미 쿠키를 발급받은 상태이므로 OO은행에 로그인이 필요하지 않기 때문이다.
SOP는 서로 다른 출처 간 리소스 공유를 제한하지만, 이 경우 리소스를 공유하는 것이 아니다. 인증된 사용자의 권한을 사용하는 것일 뿐이다.
이에 대한 해결 방법은, 먼저 Request 형식을 예측 불가능하게 만드는 방법이 있다. 예시의 OO은행이 쿠키를 Request에 포함시킨다면, 쿠키를 모르는 공격자는 올바른 요청을 전송할 수 없다.
다음으로, 해당 Request가 어디에서 왔는지 확인하여, 실제 사용자가 보낸 요청인지 확인하는 방법이 있다.
위의 두 가지 방법을 통해 CSRF를 방어하는 방법이 바로 CSRF 토큰이다.
CSRF 토큰은 사용자의 요청에 고유한 값을 포함시켜, 서버가 요청이 진짜 사용자에 의해 발생한 것인지 확인할 수 있게 한다.

이 경우 서버는 모든 요청에 대해 사용자에게 CSRF 토큰을 발급하고, 또한 검증한다.
사용자가 웹사이트에 접속하면 서버로부터 CSRF 토큰이 발급되고, 페이지를 이동하는 경우 이전 페이지에서 발급되었던 CSRF 토큰으로 인증을 수행한다. 인증이 성공할 경우, 서버는 응답과 함께 다시 새로운 CSRF 토큰을 발급한다. 해당 과정이 반복되며 지속적으로 CSRF 토큰에 대한 인증이 수행된다.
따라서 서버 측에서는 CSRF 토큰을 기억하고 있어야 하고, 저장된 토큰 값과 일치하지 않는 값이 요청으로 전송된 경우 해당 요청을 거부한다. 공격자의 경우 이전 페이지에 대한 토큰이 없기 때문에 정상적으로 요청을 수행할 수 없다.

Solution 1의 단점은 서버 측에서 모든 토큰을 기억하고 있어야 한다는 점이다.
Solution 2의 과정은 아래와 같다.
CSRF 토큰 생성
사용자가 웹사이트에 처음 방문하면, 서버는 랜덤한 CSRF 토큰을 쿠키로 발급한다. 이 쿠키는 사용자의 브라우저에 저장되어 이후 요청에 자동으로 포함된다.
토큰 읽기
웹페이지가 로드되면, 클라이언트 측 JavaScript는 쿠키에서 랜덤 토큰을 읽어 X-CSRF-TOKEN이라는 HTTP 헤더에 담아 서버로 요청을 보낼 때 함께 전송한다.
요청 전송
사용자가 웹사이트에서 요청을 전송할 때, 요청에는 쿠키와 X-CSRF-TOKEN 헤더가 포함된다. 서버는 이 두 정보를 함께 확인한다.
서버의 검증
서버는 X-CSRF-TOKEN 헤더와 쿠키의 값을 비교하여 일치하면 요청을 처리하고, 불일치하면 요청을 거부한다.
위의 방법은 서버 측에서 토큰의 값을 기억하지 않고, 단순히 클라이언트가 보내는 쿠키와 X-CSRF-TOKEN 헤더의 일치 여부만 판단하면 되기에 Solution 1에 비해 유리한 면이 있다.
위 사이트에서 XSS와 CSRF 실습을 간단하게 해보는 것을 추천한다!

쉬운 문제들부터 하나씩 풀다보면 나름 재미도 있다.