[dreamhack] Mitigation : SOP

남다은·2023년 2월 16일
0

web hacking

목록 보기
5/12

📌 SOP

동일 출처 정책, Same Origin Policy(SOP)는 클라이언트 사이드 웹 보안에 있어 중요한 요소이다.

이용자가 웹 서비스에 접속할 때, 브라우저는 해당 웹 서비스에서 사용하는 인증 정보인 쿠키를 HTTP 요청에 포함시켜 전달한다.
이러한 특징은 사이트에 직접 접속하는 것에만 한정되지 않는다.
브라우저는 웹 리소스를 통해 타 사이트를 접근할 때도 인증 정보인 쿠키를 함께 전송한다.

이러한 특징 때문에 악의적인 페이지가 클라이언트의 권한을 이용해 대상 사이트에 HTTP 응답 정보를 획득하는 코드를 실행할 수 있다.
따라서 클라이언트는 가져온 데이터를 악의적인 페이지에서 읽을 수 없게 해야 한다. 이것이 브라우저의 보안 메커니즘인 동일 출처 정책(SOP)이다.


❗️ SOP의 Origin 구분 방법

오리진(Origin)은 프로토콜(Protocol, Scheme), 포트(Port), 호스트(Host)로 구성된다.
구성 요소가 모두 일치해야 오리진이라고 한다.


SOP 실습

SOP는 Cross Origin이 아닌 Same Origin일 때만 정보를 읽을 수 있도록 한다.

Same Origin

sameNewWindow = window.open('https://dreamhack.io/lecture');
console.log(sameNewWindow.location.href);
결과: https://dreamhack.io/lecture

Cross Origin

crossNewWindow = window.open('https://theori.io');
console.log(crossNewWindow.location.href);
결과: Origin 오류 발생

다음과 같이 외부 출처에서 불러온 데이터를 읽으려고 할 때는 오류가 발생해 읽지 못한다. 하지만 읽는 것 외에 데이터를 쓰는 것은 문제없이 동작한다.


SOP 데모

다음은 모듈 구성 코드이다.

<!-- iframe 객체 생성 -->
<iframe src="" id="my-frame"></iframe>
<!-- Javascript 시작 -->
<script>
/* 2번째 줄의 iframe 객체를 myFrame 변수에 가져옵니다. */
let myFrame = document.getElementById('my-frame')
/* iframe 객체에 주소가 로드되는 경우 아래와 같은 코드를 실행합니다. */
myFrame.onload = () => {
    /* try ... catch 는 에러를 처리하는 로직 입니다. */
    try {
        /* 로드가 완료되면, secret-element 객체의 내용을 콘솔에 출력합니다. */
        let secretValue = myFrame.contentWindow.document.getElementById('secret-element').innerText;
        console.log({ secretValue });
    } catch(error) {
        /* 오류 발생시 콘솔에 오류 로그를 출력합니다. */
        console.log({ error });
    }
}
/* iframe객체에 Same Origin, Cross Origin 주소를 로드하는 함수 입니다. */
const loadSameOrigin = () => { myFrame.src = 'https://same-origin.com/frame.html'; }
const loadCrossOrigin = () => { myFrame.src = 'https://cross-origin.com/frame.html'; }
</script>
<!--
버튼 2생성 (Same Origin 버튼, Cross Origin 버튼)
-->
<button onclick=loadSameOrigin()>Same Origin</button><br>
<button onclick=loadCrossOrigin()>Cross Origin</button>
<!--
frame.html의 코드가 아래와 같습니다.
secret-element라는 id를 가진 div 객체 안에 treasure라고 하는 비밀 값을 넣어두었습니다.
-->
<div id="secret-element">treasure</div>

코드 동작 설명

myFrame.onload = () => {
    /* try ... catch 는 에러를 처리하는 로직 입니다. */
    try {
     ...
  • onload는 이벤트 핸들러로, 객체가 성공적으로 로드된 경우 동작한다. 위의 코드에서는 iframe 객체에 페이지가 로드되면 동작하게 된다.
        let secretValue = myFrame.contentWindow.document.getElementById('secret-element').innerText;
        console.log({ secretValue });
  • 로드가 완료되면 iframe 내에 삽입된 주소에서 secret-element객체의 값인 treasure를 읽어와 콘솔에 출력한다.

=> 실습을 통해 확인하면 same origin 버튼을 누르면 코드가 성공적으로 수행되어 콘솔에 treasure이 출력되는 걸 확인할 수 있고, cross origin 버튼을 누르면 Cross origin 데이터에 접근할 수 없다는 오류가 출력되는 걸 확인할 수 있다.


📌 CORS

브라우저가 SOP에 구애받지 않고 외부 출처에 대한 접근을 허용해주는 경우가 존재한다. 예를 들어 이미지나 자바스크립트, CSS 등의 리소스를 불러오는 <img>, <style>, <script> 등의 태그는 SOP의 영향을 받지 않는다.

이런 경우 외에도 웹 서비스에서 동일 출처 정책인 SOP를 완화하여 다른 출처의 데이터를 처리해야 하는 경우도 있다. 이때 두 사이트는 오리진이 다르기 때문에 SOP를 적용받지 않고 리소스를 공유할 방법이 필요하다.

위와 같은 상황에서 자원을 공유하기 위해 사용할 수 있는 공유 방법을 교차 출처 리소스 공유(Cross Origin Resource Sharing, CORS)라고 한다.

교차 출처의 자원을 공유하는 방법은 CORS와 관련된 HTTP헤더를 추가하여 전송하는 방법을 사용한다.
이외에도 JSON with Padding(JSONP) 방법을 통해 CORS를 대체할 수 있다.

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


다음은 웹 리소스를 요청하는 발신측 코드의 일부와 발신측의 HTTP 요청이다.

웹 리소스를 요청하는 발신측 코드
/*
    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/

발신측에서 POST 방식으로 HTTP 요청을 보냈으나, OPTIONS 메소드를 가진 HTTP 요청이 전달된 것을 확인할 수 있다.
이를 CORS preflight라고 하며, 수신측에 웹 리소스를 요청해도 되는지 질의하는 과정이다.

Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type

발신측의 HTTP 요청 코드를 살펴봤을 때, "Access-Control-Request"로 시작하는 헤더가 존재하는 걸 확인할 수 있다.
해당 헤더 뒤에 Method와 Headers는 각각 메소드와 헤더를 추가적으로 사용할 수 있는지를 질의한다.

다음과 같이 질의했을 때 서버는 다음과 같이 응답한다.

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


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


📌 JSONP

이미지나 자바스크립트, CSS 등의 리소스가 SOP에 구애받지 않고 외부 출처에 대해 접근을 허용한다는 점을 이용해서 JSONP 방식은 <script>태그로 Cross Origin의 데이터를 불러온다.
하지만 <script> 태그 내에서는 자바스크립트의 코드로 인식하기 때문에 Callback함수를 사용해야 한다. callback 파라미터에 어떤 함수로 받아오는 데이터를 핸들링할지 넘겨주면, 대상 서버는 전달된 Callback으로 데이터를 감싸 응답한다.

예시_

<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>

다음의 코드를 살펴보면 마지막 줄에서 Cross Origin의 데이터를 불러오고, 이때 callback 파라미터로 myCallback을 함께 전달하는 걸 확인할 수 있다.
Cross Origin에서는 응답할 데이터를 myCallback함수의 인자로 전달될 수 있도록 myCallback으로 감싸서 JavaScript 코드를 반환해준다.

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

다만 JSONP는 CORS가 생기기 전에 사용하던 방법으로, 현재는 거의 사용하지 않는다.

profile
차곡차곡 쌓아나가는 WEB FE 개발자

0개의 댓글