Mitigation: Same Origin Policy
# 들어가며 - 서론
- 쿠키: 인증 상태를 나타내는 민감한 정보가 보관되며, 브라우저 내부에 저장됨
- 브라우저: 웹 서비스에 접속할 때 자동으로 쿠키를 헤더에 포함시켜 요청을 보냄
- 쿠키와 브라우저 덕분에 포털 사이트, SNS와 같은 웹 서비스에 한 번 로그인한 후 일정 기간은 로그인하지 않고도 바로 서비스를 사용할 수 있음
- but, 이용자가 악의적인 페이지를 접속했을 때 페이지가 자바스크립트를 사용해 이용자의 SNS 웹 서비스로 요청을 보낸다면...
> 브라우저는 요청을 보낼 때 헤더에 해당 웹 서비스 쿠키를 포함시킬 것임
> 자바스크립트로 요청을 보낸 페이지는 로그인된 이용자의 SNS 응답을 받을 것
> 마음대로 페이지 접속자의 SNS에 글을 올리거나, 삭제하고, SNS 메신저를 읽는 것도 가능
- 위와 같은 문제를 방지하기 위해 동일 출처 정책, Same Origin Policy(SOP) 보안 메커니즘이 탄생
> 클라이언트 사이드 웹 보안에 있어 중요한 요소
> 클라이언트 사이드 공격은 이 SOP를 우회하기 위한 것이라고 볼 수 있음
# Same Origin Policy (SOP)
1. Same Origin Policy (SOP)
- 브라우저: 인증 정보로 사용될 수 있는 쿠키를 브라우저 내부에 보관 + 이용자가 웹 서비스에 접속할 때, 해당 웹 서비스에서 사용하는 인증 정보인 쿠키를 HTTP 요청에 포함시켜 전달
> 위와 같은 특징은 사이트에 직접 접속하는 것에만 한정되지 않음
> 브라우저는 웹 리소스를 통해 간접적으로 타 사이트에 접근할 때도 인증 정보인 쿠키를 함께 전송하는 특징을 가짐
- 위와 같은 특징 때문에 악의적인 페이지가 클라이언트의 권한을 이용해 대상 사이트에 HTTP 요청을 보내고, HTTP 응답 정보를 획득하는 코드를 실행할 수 있음
> 이는 정보 유출과 같은 보안 위협이 생길 수 있는 요소가 됨
- 따라서 클라이언트 입장에서는 가져온 데이터를 악의적인 페이지에서 읽을 수 없도록 해야함
> 이것이 브라우저의 보안 메커니즘인 통일 출처 정책 (Same Origin Policy, SOP)임
2. Same Origin Policy의 오리진 (Origin) 구분 방법
- 오리진: 프로토콜(Protocol, Scheme), 포트(Port), 호스트(Host)로 구성
- 구성 요소가 모두 일치해야 동일한 오리진
- https://same-origin,com/라는 오리진과 아래 URL을 비교했을 때 결과는 다음과 같음
3. Same Origin Policy 실습
- SOP는 Cross Origin이 아닌 Same Origin일 때만 정보를 읽을 수 있도록 해줌
- https://dreamhack.io 에서 javascript를 이용해 SOP를 테스트 하는 코드
> window.open: 새로운 창을 띄우는 함수
> object.location.href: 객체가 가리키고 있는 URL 주소를 읽어오는 코드
- Same Origin
sameNewWindow = window.open('https://dreamhack.io/lecture');
console.log(sameNewWindow.location.href);
결과: https://dreamhack.io/lecture
새로운 창도 잘 뜨고 log도 잘 출력된다
- Cross Origin
crossNewWindow = window.open('https://theori.io');
console.log(crossNewWindow.location.href);
결과: Origin 오류 발생
새로운 창은 잘 뜨지만, 데이터를 읽어오지 못하는 것을 확인할 수 있다
- Cross Origin 데이터 읽기/쓰기
> 외부 출처에서 불러온 데이터를 읽으려고 할 때는 오류가 발생해 읽지 못함
> 읽는 것 외에 데이터를 쓰는 것은 문제 없이 동작
crossNewWindow = window.open('https://theori.io');
crossNewWindow.location.href = "https://dreamhack.io";
4. Same Origin Policy (SOP) 데모
- 코드 동작 설명
1) 두 번째 줄의 iframe은 현재 웹 페이지 안에 또 다른 하나의 웹 페이지를 삽입하는 HTML 태그
src 요소를 설정함으로써 삽입할 웹 페이지의 주소가 결정됨
2) 10번째 줄의 onload는 이벤트 핸들러로써, 해당 객체가 성공적으로 로드되었을 때 동작
본 예시에서는 10~23번 줄이 iframe 객체에 페이지가 로드되면 동작하는 코드
3) 14~15번째 줄은 로드가 완료되면 iframe 내에 삽입된 주소에서 secret-element 객체의 값인
treasure를 읽어와 콘솔에 출력하는 동작을 수행
<!-- 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에 Same Origin이 아닌 Cross Origin을 넣었다면 세 번째 동작 과정이 실패한 것
- Same Origin 버튼: 앞선 과정이 모두 성공적으로 수행되어 treasure라는 값이 출력됨
- Cross Origin 버튼: 앞선 과정 중, 세 번째 과정이 실패하여 Cross Origin의 데이터에 접근할 수 없다는 오류 출력
# Cross Origin Resource Sharing (CORS)
1. Same Origin Policy 제한 완화
- SOP는 클라이언트 사이드 웹 보안에서 중요한 요소
- but, 브라우저가 이러한 SOP에 구애 받지 않고 외부 출처에 대한 접근을 허용해주는 경우가 존재
ex) <img>, <style>, <script> 태그는 SOP의 영향을 받지 않음
- 웹 서비스에서 동일 출처 정책인 SOP를 완화하여 다른 출처의 데이터를 처리해야하는 경우도 있음
ex) 특정 사이트가 카페, 블로그, 메일 서비스를 아래의 주소로 운영할 경우
- 카페: https://cafe.dreamhack.io
- 블로그: https://blog.dreamhack.io
- 메일: https://mail.dreamhack.io
- 메인: https://dreamhack.io
이러한 환경에서, 이용자가 수신한 메일의 개수를 메인 페이지에 출력하려면,
개발자는 메인 페이지에서 메일 서비스에 관련된 리소스를 요청하도록 해야함
이때 두 사이트는 오리진이 다르므로 SOP를 적용받지 않고 리소스를 공유할 방법이 필요
- 위와 같은 상황에서 자원을 공유하기 위해 사용할 수 있는 방법을 교차 출처 리소스 공유(Cross Origin Resource Sharing, CORS)라고 함
- 교차 출처의 자원 공유하는 방법은 CORS와 관련된 HTTP 헤더를 추가하여 전송하는 방법을 사용
- 이외에도 JSON with Padding(JSONP) 방법을 통해 CORS를 대체할 수 있음
2. Cross Origin Resource Sharing (CORS)
- 교차 출처 리소스 공유 (Cross Origin Resource Sharing, 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 요청
> 발신측에서 POST 방식으로 HTTP 요청을 보냈으나,
OPTIONS 메소드를 가진 HTTP 요청이 전달된 것을 확인할 수 있음
> 이를 CORS preflight라고 하며, 수신측에 웹 리소스를 요청해도 되는지 질의하는 과정
> "Access-Control-Request"로 시작하는 헤더가 존재하는 것을 확인할 수 있음
> 해당 헤더 뒤에 따라오는 Method와 Headers는 각각 메소드와 헤더를 추가적으로 사용할 수 있는지 질의
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/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 요청을 보냄
3. JSON with Padding (JSONP)
- 이미지나 자바스크립트, CSS 등의 리소스는 SOP에 구애 받지 않고 외부 출처에 대해 접근을 허용함
- JSON 방식은 이러한 특징을 이용해 <script> 태그로 Cross Origin의 데이터를 불러옴
> 하지만 <script> 태그내에서는 데이터를 자바스크립트의 코드로 인식하기 때문에 Callback 함수를 활용해야 함
- Cross Orgin에 요청할 때 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
13번째 줄: Cross Origin의 데이터를 불러옴. 이때, callback 파라미터로 myCallback을 함게 전달.
Cross Origin에서는 응답할 데이터를 myCallback 함수의 인자로 전달될 수 있도록
myCallback으로 감싸 javascript 코드를 반환해 줌
3~6번째 줄: 위에서 반환된 코드가 요청측에서 실행되기 때문에
3~6번째 줄에서 정의된 myCallback 함수가 전달된 데이터를 읽을 수 있음
- 예시: 웹 리소스 요청에 따른 응답 코드
/*
수신측은 myCallback 이라는 함수를 통해 요청측에 데이터를 전달합니다.
전달할 데이터는 현재 theori.io에서 클라이언트가 사용 중인 계정 정보인
{'id': 'dreamhack'} 입니다.
*/
myCallback({'id':'dreamhack'});
- JSONP는 CORS가 생기기 전에 사용하던 방법으로 현재는 거의 사용하지 않음
# 마치며
- 키워드
① Same Origin Policy (SOP): 동일 출처 정책, 현재 페이지의 출처가 아닌 다른 출처로부터 온 데이터를 읽지 못하게 하는 브라우저의 보안 메커니즘
② Same Origin: 현재 페이지와 동일한 출처
③ Cross Origin: 현재 페이지와 다른 출처
④ Cross Origin Resource Sharing (CORS): 교차 출처 리소스 공유, SOP의 제한을 받지 않고 Cross Origin의 데이터를 처리할 수 있도록 해주는 메커니즘
'Hacking Tech > Web hacking' 카테고리의 다른 글
[Dreamhack] ClientSide: CSRF (0) | 2022.02.13 |
---|---|
[Dreamhack] ClientSide: XSS (0) | 2022.01.27 |
[Dreamhack] Background: Cookie & Session (0) | 2022.01.24 |
[Dreamhack] Tools: Browser DevTools (0) | 2022.01.23 |
[Dreamhack] Background: Web Browser (0) | 2022.01.22 |