테스트 환경

Web1 Web2
192.168.10.130 192.168.10.131
test1.com test2.com

Web1 서버에서의 테스트 코드 (test.php)

<!DOCTYPE html>
<html>
    <head>
        <script>
            function cors() {
                var xhttp = new XMLHttpRequest();
                xhttp.onreadystatechange = function() {
                    if (this.readyState == 4 && this.status == 200) {
                        alert(this.responseText);
                    }
                };
                xhttp.open("GET", "http://test2.com/secret.txt", true);
                xhttp.send();
            };
        </script>
    </head>

    <body onload="cors()">
        <h2>CORS Exploit</h2>
        <h3>test2.com's secret</h3>
    </body>
</html>

Web2 서버에서의 테스트 코드 (secret.php)

secret number : 1234

CORS 정책 위반 상황

Web1 서버의 테스트 코드는 test.php의 <body> 태그 안에 있는 텍스트들을 화면에 출력하며 Web2 서버로부터 secret.txt를 불러와 alert 창에 내용을 출력하게 되어 있다.

하지만 아래처럼 test.php에 접속하면 <body> 태그 안의 내용은 나타나지만, alert 창은 나타나지 않는다.

클라이언트에서 웹서버1의 test.php에 접속했을 시 출력 내용


크롬 브라우저의 콘솔을 확인하면 아래와 같은 메시지가 나오며, secret.txt 로드에 실패한다.

크롬 브라우저의 콘솔 확인


클라이언트 <-> 웹서버1 간 HTTP 패킷 흐름


패킷을 확인해보면 클라이언트-Web1 서버, 클라이언트-Web2 서버 간의 통신엔 큰 문제는 없다.

그리고 Web2 서버는 분명히 클라이언트에게 secret.txt를 보내주었음을 알 수 있다.

192.168.10.1 (클라이언트) -> 192.168.10.130 (Web1 서버) HTTP 요청


192.168.10.130 (Web1 서버) -> 192.168.10.1 (클라이언트) HTTP 응답


192.168.10.1 (클라이언트) -> 192.168.10.131 (Web2 서버) HTTP 요청


192.168.10.131 (Web2 서버) -> 192.168.10.1 (클라이언트) HTTP 응답


하지만 크롬 브라우저에서는 secret.txt를 보여주지 않는다. 이를 통해 CORS 정책을 확인하는 것은 서버가 아닌 클라이언트의 브라우저가 담당하고 있음을 알 수 있다.

test.php에 대한 HTTP 헤더


test.php에 대한 Response


secret.php에 대한 HTTP 헤더


secret.php에 대한 Response

CORS 정책 위반 시나리오 정리

위 내용을 정리하자면 클라이언트는 Web1 서버에게 test.php 페이지를 요청했고, Web1 서버는 test1.php의 대한 html 출력 값을 클라이언트에게 응답한다.

해당 html 코드에는 Web2 서버로부터 secret.txt를 불러와 alert 창에 내용을 출력하는 내용이 담겨 있다. 클라이언트는 Web2 서버에 secret.txt을 요청했으며, 파일을 불러오는 것까진 성공했다. 하지만 브라우저 (여기선 크롬)에서 해당 secret.txt 값을 출력하는 것을 거부하는 상황에 놓인다.

위 내용을 바탕으로 test1.com/test.php 페이지가 출력되는 과정을 정리해보자.

클라이언트(크롬 브라우저)는 Web1 서버에게 test.php 페이지에 대한 응답을 요청한다.


Web1 서버는 클라이언트에게 test.php에 대한 응답을 html 문서로 보내준다.



클라이언트는 응답으로 받은 html 문서에서 외부 데이터를 다시 받아야 한다는 것을 알게 된다 (12번째 줄) Web2 서버에게 secret.php 를 요청한다. 이때 요청 헤더에 Origin이 추가되어 있는 것을 알 수 있다. 이는 secret.php를 불러오려는 출처를 의미한다.


Web2 서버는 secret.txt를 응답으로 보낸다.


클라이언트(크롬 브라우저)는 secret.php를 받지만 출력하지 않는다. Web2 서버에 요청 헤더에 있는 Origin 값과 secret.php를 받아온 Web2 서버의 URL 값 (http://test2.com) 이 다르기 때문에 CORS 정책 위반으로 크롬 브라우저는 secret.php 값을 로드하지 않는다.

해결 방법

해결 방법은 크게 2가지 방법이다.

첫 번째는 소스 코드를 수정하는 방법과 두 번째는 웹 서버 또는 CDN 설정에 Access-Control-Allow-Origin를 추가하는 것이다.

Access-Control-Allow-Origin 응답 헤더는 이 응답이 주어진 origin으로부터의 요청 코드와 같이 사용될 수 있는지 여부를 나타낸다.


[소스 코드를 이용한 해결 방법]

Web2 서버에서 secret.php 파일에 아래의 코드 추가

<?
  header("Access-Control-Allow-Origin: http://test1.com");
?>


[웹 서버 설정을 이용한 해결 방법]

Web2 서버의 httpd.conf 에 , , , 4개의 태그 중 한 곳에 아래의 내용을 추가한 후 아파치 재실행

<Directory "/home/httpd/html">
	Header set Access-Control-Allow-Origin "http://test1.com"   (추가)


브라우저 출력 결과


이전과 달리 응답 헤더에 Access-Control-Allow-Origin 값이 있으며, secret.php도 로드한다.



정리

CORS는 다른 도메인 주소의 이미지 파일이나 CSS, 자바스크립트 라이브러리 등을 마구잡이로 받아오는 것을 막기 위해 클라이언트(브라우저)에서 확인하는 정책이다.

CORS 정책을 통해 클라이언트에서 외부 리소스를 읽어오는 것을 통제할 수 있으며, 이는 XSS나 CSRF 등과 같이 검증되지 않은 외부 리소스를 읽어오면서 발생하는 웹 취약점을 예방할 수 있다.

참고자료

  • [비박스] Security Misconf - CORS (Ajax) : CORS Exploit (코드 출처)
    • https://m.blog.naver.com/PostView.nhn?blogId=is_king&logNo=221619709756&categoryNo=89&proxyReferer=&proxyReferer=https:%2F%2Fwww.google.com%2F
  • 아파치에서 access-control-allow-origin 설정하는 방법
    • https://ubiq.co/tech-blog/set-access-control-allow-origin-cors-headers-apache/

Tags: ,

Categories:

Published:

Updated: