Web Hacking - Same Origin Policy

쿠키에는 인증 상태를 나타내는 민감한 정보가 보관되며, 이는 브라우저 내부에 저장된다. 혹, 악의적인 페이지가 이용자의 SNS 웹 서비스로 요청을 보낸다면 어떻게 될까 ? 브라우저는 요청을 보낼 때 헤더에 해당 웹 서비스의 쿠키를 포함시키므로, 해당 페이지는 이용자의 SNS 응답을 받고, 글을 올리고, 삭제하는 등의 작업이 가능할 것이다.

SOP (Same Origin Policy : 동일 출처 정책)

브라우저는 사이트에 직접적으로 접속할 때뿐만 아니라, 웹 리소스를 통해 간접적으로 타 사이트에 접근할 때에도 쿠키를 함께 전송한다. 이 때문에 위와 같은 악의적인 사용이 일어날 수 있는데, 이 때 보안 위협을 막기 위해 사용되는 것이 브라우저의 보안 매커니즘인 SOP 이다.클라이언트 사이드 웹 보안에서 중요한 위치를 지니며, 사실상 클라이언트 사이드 공격은 SOP 를 우회하기 위한 것이라고 말할 수 있다.

SOP의 Origin 구별 방법

Origin은 Protocol, Scheme, Port, Host 네 가지로 구성된다. 이 때, 모든 구성요소가 일치해야 동일한 origin으로 판단되며, SOP는 Same Origin일 때만 정보를 읽을 수 있게 해준다.

*https://dramhack.io 에서 콘솔을 통해 실습한 결과

console.log(crossNewWindow.location.href);

위 코드의 오류문에서 외부 출처에서 불러온 데이터는 Origin 오류가 발생하여 읽을 수 없는 것을 확인할 수 있다.

crossNewWindow = window.open("https://theori.io");

그러나 이와 같이 데이터를 쓰는 코드는 문제 없이 동작하는 것을 볼 수 있다.

실습

  • SOP

CORS (Cross Origin Resource Sharing)

Same Origin Policy 제한 완화

브라우저는 SOP에 구애받지 않고 외부 출처에 대한 접근을 허용해주는 경우가 존재한다 (img, style, script 등의 리소스를 불러오는 태그)

추가적으로, 특정 포털 사이트가 아래와 같이 카페, 블로그, 메일 서비스 등을 다른 호스트로 운영할 경우 브라우저는 각 사이트의 origin이 다르다고 인식한다.

  • https://cafe.dreamhack.io
  • https://blog.dreamhack.io
  • https://mail.dreamhack.io
  • https://dreamhack.io

이러한 환경에서 이용자가 수신한 메일의 개수를 메인 페이지에 출력하려면, 메인페이지에서 메일 서비스에 관련된 리소스를 요청해야 한다. 이 때, 두 사이트는 origin이 다르기에, SOP을 완화하여 리소스를 공유할 방법이 필요하다.

CORS

위와 같은 상황에서 자원을 공유할 때 사용하는 공유기법으로, 교차 출처 리소스 공유라고 일컫는다. 교차 출차의 자원을 공유하려면, CORS와 관련된 HTTP 헤더를 추가하여 전송해야하며, 이 외에도 JSONP(JSON with Padding)로 CORP를 대체할 수 있다.

CORS : HTTP 헤더에 기반하여 Cross Origin 간에 리소스를 공유하는 방법
발신 측에서 CORS 헤더를 설정하여 요청하면, 수신 측에서 헤더를 구분하여 정해진 규칙에 맞게 데이터를 가져갈 수 있도록 설정한다.

*웹 리소스를 요청하는 발신 측의 코드

/*
    XMLHttpRequest 객체를 생성합니다. 
    XMLHttpRequest는 웹 브라우저와 웹 서버 간에 데이터 전송을
    도와주는 객체 입니다. 이를 통해 HTTP 요청을 보낼 수 있습니다.
*/
xhr = new XMLHttpRequest();
/* https://theori.io/whoami 페이지에 POST 요청을 보내도록 합니다. */
xhr.open("POST", "https://theori.io/whoami");
/* HTTP 요청을 보낼 때, 쿠키 정보도 함께 사용하도록 해줍니다. */
xhr.withCredentials = true;
/* HTTP Body를 JSON 형태로 보낼 것이라고 수신측에 알려줍니다. */
xhr.setRequestHeader("Content-Type", "application/json");
/* xhr 객체를 통해 HTTP 요청을 실행합니다. */
xhr.send("{'data':'WhoAmI'}");


*웹 리소스를 요청하는 발신 측의 HTTP 요청

OPTIONS /whoami HTTP/1.1
Host: theori.io
Connection: keep-alive
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Origin: https://dreamhack.io
Accept: */*
Referer: https://dreamhack.io/

CORS preflight : 수신 측에 웹 리소스를 요청해도 되는 지 질의하는 과정 (예비 요청)으로, 본 요청을 보내도 안전할 지 브라우저가 판단하기 위한 용도이다 - OPTIONS 메소드 사용

위의 요청 코드와 요청문에서, 발신 측에서 POST 메소드로 HTTP 요청을 보냈으나 HTTP 요청은 OPTIONS 메소드로 전달된 것을 확인할 수 있다.

*서버의 응답

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://dreamhack.io
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type
Header Description
Access-Control-Allow-Origin 헤더 값에 해당하는 Origin에서 들어오는 요청만 처리한다
예시의 경우 : https://dreamhack.io
Access-Control-Allow-Methods 헤더 값에 해당하는 메소드의 요청만 처리한다
예시의 경우 : POST, GET, OPTIONS
Access-Control-Allow-Credentials 쿠키의 사용 여부를 판단한다
예시의 경우 : true 이므로, 쿠키의 사용을 허용함
Access-Control-Allow-Headers 헤더 값에 해당하는 헤더의 사용 가능 여부를 나타냄


위의 과정을 마치면 브라우저는 수신 측의 응답이 발신 측과 상응하는 지 확인하고, 그 후에 POST 메소드로 요청을 보내 수신 측의 웹 리소스를 요청한다.

JSONP

JS, CSS 등의 리소스가 SOP에 구애받지 않는다는 특징을 이용하여 script 태그를 사용하여 Cross Origin의 데이터를 불러온다. 그러나 해당 태그 안에서는 데이터가 JS 코드로 인식되기 때문에, Callback 함수를 사용해야한다.

Cross Origin에 보내는 요청에서 callback 파라미터에 어떤 함수로 받아오는 데이터를 핸들링할 지 넘겨주면, 대신 서버는 전달된 Callback으로 데이터를 감싸서 응답한다.

그러나, JSONP는 CORS가 생기기 전에 사용하던 방법으로, 현재는 거의 사용하지 않기에, 새로운 코드를 작성할 때에는 CORS를 사용해야 한다.

*웹 리소스 요청 코드

<script>
  /* myCallback이라는 콜백 함수를 지정합니다. */
  function myCallback(data) {
    /* 전달받은 인자에서 id를 콘솔에 출력합니다.*/
    console.log(data.id);
  }
</script>
<!--
https://theori.io의 스크립트를 로드하는 HTML 코드입니다.
단, callback이라는 이름의 파라미터를 myCallback으로 지정함으로써
수신측에게 myCallback 함수를 사용해 수신받겠다고 알립니다.
-->
<script src="http://theori.io/whoami?callback=myCallback"></script>


*응답 코드

/*
수신측은 myCallback 이라는 함수를 통해 요청측에 데이터를 전달합니다.
전달할 데이터는 현재 theori.io에서 클라이언트가 사용 중인 계정 정보인
{'id': 'dreamhack'} 입니다. 
*/
myCallback({ id: "dreamhack" });