Recent Posts
Recent Comments
Link
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Tags
more
Archives
Today
Total
관리 메뉴

CIDY

[Web_Hacking] stage3_Same Origin Policy(SOP) 본문

Hack/DreamHack(로드맵)

[Web_Hacking] stage3_Same Origin Policy(SOP)

CIDY 2022. 7. 27. 22:20

어떤 악의적인 페이지가 JS를 이용해 클라이언트의 SNS웹 서비스로 요청을 보냄 -> 브라우저는 요청 보낼 때 헤더에 해당 웹 서비스의 쿠키를 포함시킴 -> 악의적인 페이지는 로그인 된 이용자의 SNS응답을 받음 -> 해당 SNS페이지를 마음대로 할 수 있음

 

-> 그래서 나온 게 SOP(동일 출처 정책)

 

 

*Same Origin Policy(SOP)

위에서 말했듯 악의적인 사이트가 특정 페이지에 HTTP요청을 보내고(이 때 헤더에 쿠키가 포함됨) -> HTTP응답 정보를 획득하는 코드를 실행할 수 있다. -> 가져온 데이터를 악의적인 페이지에서 읽을 수 없도록 할 필요가 있음 -> SOP

 

그럼 정보의 출처(Origin)를 어떻게 구분할까? -> 오리진은 프로토콜/포트/호스트로 구성 -> 세 요소가 모두 일치해야 동일한 오리진이라고 함.

 

 

Scheme == Protocol이다. https:// 부분이 프로토콜.

호스트는 뒷내용을 말하고, 지금은 다르기 때문에 크로스 오리진으로 판정된다.

포트는 둘 다 https표준인 443이다.

 

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

 

 

 

https://dreamhack.io에서 

 

crossNewWindow = window.open('https://theori.io');
console.log(crossNewWindow.location.href);

 

위와 같은 코드를 통해 외부 사이트에서 데려온 데이터를 읽으려고 하면 아래와 같은 오류 메시지가 뜬다.

 

 

 

 

하지만 데이터를 읽어오는 것만 안될 뿐 위와 같이 데이터를 쓰는 것은 가능하다. -> 위 코드를 실행시키면 새 창(티오리)이 생성되었다가 dreamhack.io사이트로 바뀐다.

 

 

 

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

 

위쪽이 iframe은 현재 웹 페이지 안에 또다른 웹페이지를 삽입하는 HTML태그이다.

 

myFrame.onload는 이벤트 핸들러인데, 해당 객체가 성공적으로 로드되었을 때 동작한다.

 

(const loadCrossOrigin = () => { myFrame.src = 'https://cross-origin.com/frame.html'; }까지가 iframe객체에 페이지가 로드되면 동작하는 코드이다.)

 

let secretValue = myFrame.contentWindow.document.getElementById('secret-element').innerText; console.log({ secretValue }); -> 이 부분은 로드가 완료되면 iframe내에 삽입된 주소에서 secret-element 객체의 값인 treasure를 읽어와 콘솔에 출력하는 동작을 수행한다.

 

 

위 코드로 동작하는 모듈에서 same orgin을 누르면 secretValue가 잘 출력되지만, cross origin을 누르면 크로스오리진 에러메시지가 발생한다. -> iframe내에 삽입된 주소에서 값을 읽어올 수 없는 것이다.

 

 

 

 

*CORS(Cross Origin Resource Sharing)

브라우저가 SOP에 구애받지 않고 외부 출처에 대한 접근을 허용하는 경우가 있음. -> 이미지/JS/CSS와 같은 리소스를 불러오는 <img>, <style>, <script>와 같은 태그는 SOP의 영향을 받지 않는다.

 

또한 SOP를 완화해 다른 출처로부터의 데이터를 처리해야 하는 경우도 있다. 가령 어떤 포털 사이트에서 카페/블로그/메일/검색포털 등과 같은 여러 서비스를 운영하고 있다고 하자. 각 서비스는 Host가 달라 크로스 오리진이다.

 

그런데 네이버같은데서 보면 검색창이 있는 메인 페이지에서도 메일의 제목과 약간의 내용, 수신한 메일 목록 등을 확인할 수 있는 것을 볼 수 있다. -> 두 사이트는 오리진이 다르다 -> SOP를 적용받지 않고 리소스를 공유해야 함 -> CORS

 

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

 

CORS의 경우 송신측에서 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'}");

 

이건 웹 리소스 요청 코드다. 

 

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/

 

그리고 이건 발신측의 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요청을 보냈지만 OPTIONS메소드를 가진 HTTP요청이 전달된 것을 볼 수 있다. -> CORS preflight라고 함. (수신측에 웹 리소스 요청 가능 여부를 질의하는 과정)

 

Access-Control-Allow-Origin -> 해당 값에 해당하는 오리진에서 들어오는 요청만 처리

Access-Control-Allow-Method -> 헤더 값에 해당하는 메소드의 요청만 처리

Access-Control-Allow-Credentials -> 쿠키 사용 여부 판단. (위의 경우 쿠키 사용 허가)

Access-Control-Allow-Headers -> 헤더 값에 해당하는 헤더의 사용 가능 여부

 

 

위 과정 이후 브라우저는 수신측 응답이 발신측 요청과 상응하는지 확인을 거친 뒤 POST요청을 보내 수신측의 웹 리소스를 요청하는 HTTP요청을 보낸다.

 

 

 

 

*JSONP(JSON with Padding)

이미지/JS/CSS와 같은 리소스는 SOP와 관계없이 외부 출처에 대해 접근을 허용 -> JSONP는 이를 이용해 <script>태그로 크로스 오리진 데이터를 불러온다. -> 근데 이 태그는 데이터를 JS코드로 인식하기에 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>

 

마지막 줄을 보면 크로스오리진의 데이터를 불러온다. -> callback의 파라미터로 myCallback을 전달함 -> 크로스오리진은 응답 데이터를 myCallback함수의 인자로 전달될 수 있도록 myCallback으로 감싸서 JS코드를 반환 -> 반환 코드는 요청측에서 실행되므로 위쪽에 정의된 myCallbask함수가 받아온 데이터를 읽을 수 있다.

 

 

아래는 위의 요청에 따른 응답 코드이다.

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

 

 

 

(*JSONP는 CORS 이전의 방식으로 현재는 거의 사용하지 않음)