브라우저는 인증 정보로 사용될 수 있는 쿠키를 브라우저 내부에서 보관함
이용자가 웹 서비스에 접속할 때, 브라우저는 해당 웹 서비스에서 사용하는 인증 정보인 쿠키를 HTTP 요청에 포함시켜 전달함
브라우저는 타 사이트에 접근할 때도 인증 정보인 쿠키를 함께 전송하는 특징을 가짐
→ 악의적인 페이지가 클라이언트의 권한을 이용해 대상 사이트에 HTTP 요청을 보내고, HTTP 응답 정보를 획득 하는 코드를 실행할 수 있음
→ 클라이언트 입장에서는 가져온 데이터를 악의적인 페이지에서 읽을 수 없도록 해야함 ⇒ 브라우저의 보안 메커니즘인 동일 출처 정책(Same Origin Policy, SOP)
오리진은 프로토콜(Protocol, Scheme), 포트(Port), 호스트(Host)로 구성됨
구성 요소가 모두 일치해야 동일한 오리진임
ex) https://same-origin.com/
라는 오리진과 아래 URL 비교
URL | 결과 | 이유 |
---|---|---|
https://same-origin.com/frame.html | Same Origign | Path만 다름 |
http://same-origin.com/frame.html | Cross Origin | Scheme이 다름 |
https://cross.same-origin.com/frame.html | Cross Origin | Host가 다름 |
https://same-origin.com:1234/ | Cross Origin | Port가 다름 |
SOP는 Cross Origin이 아닌 Same Origin일 때만 정보를 읽을 수 있도록 해줌
[window.open](http://window.open)
→ 새로운 창을 띄우는 함수
object.location.href
→ 객체가 가리키고 있는 URL 주소를 읽어오는 코드
<-!-- iframe 객체 생성 -->
// 현재 웹 페이지 안에 또 다른 하나의 웹 페이지를 삽입하는 HTML 태그
// src 요소를 설정함으로써 삽입할 웹 페이지의 주소가 결정됨
<iframe src=""
id="my-frame">
</iframe>
<-!-- Javascript 시작 -->
<script>
/* 2번째 줄의 iframe 객체를 myFrame 변수에 가져옴 */
let myFrame = document.getElementById('my-frame')
/* iframe 객체에 주소가 로드되는 경우 아래와 같은 코드를 실행함. */
// onload는 이벤트 핸들러로써, 해당 객체가 성공적으로 로드되었을 때 동작함
myFrame.onload = () => {
/* try... catch는 에러를 처리하는 로직 */
try {
/* 로드가 완료되면, secret-element 객체의 내용을 콘솔에 출력 */
// 로드가 완료되면 iframe내에 삽입된 주소에서 secret-element 객체의 값인 treasure를 읽어와 콘솔에 출력하는 동작을 수행함
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()>SameOrigin</button>
<br>
<button
onclick=loadCrossOrigin()>CrossOrigin</button>
<-!--
frame.html의 코드가 아래와 같다. secret-element라는 id를 가진 div 객체 안에 treasure라고 하는 비밀 값을 넣어두었다.
-->
<div id="secret-element">treasure</div>
SOP는 클라이언트 사이드 웹 보안에서 중요한 요소지만, 브라우저가 이러한 SOP에 구애 받지 않고 외부 출처에 대한 접근을 허용해주는 경우가 존재함
이미지나 자바스크립트, CSS 등의 리소스를 불러오는 <img>
, <style>
, <script>
등의 태그는 SOP의 영향을 받지 않음
웹 서비스에서 동일 출처 정책인 SOP를 완화해 다른 출처의 데이터를 처리 해야 하는 경우도 존재함
→ 특정 포털 사이트가 카페, 블로그, 메일 서비스를 아래의 주소로 운영하고 있다고 할 때, 각 서비스의 Host가 다르기 때문에 브라우저는 각 사이트의 origin이 다르다고 인식함
이러한 환경에서 이용자가 수신한 메일의 개수를 메인 페이지에 출력하려면, 개발자는 메인 페이지에서 메일 서비스에 관련된 리소스를 요청하도록 해야함.
이때, 두 사이트는 origin이 다르므로 SOP를 적용받지 않고 리소스를 공유 할 방법이 필요함
⇒ 그 방법이 교차 출처 리소스 공유(Cross Origin Resource Sharing, CORS)
CORS와 관련된 HTTP 헤더를 추가해 전송하는 방법을 사용
+) JSON with Padding(JSONP)
방법을 통해 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/
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 | 설명 |
---|---|
Access-Control-Allow-Origin | 헤더 값에 해당하는 Origin에서 들어오는 요청만 처리 |
Access-Control-Allow-Methods | 헤더 값에 해당하는 Method의 요청만 처리 |
Access-Control-Allow-Credentials | 쿠키 사용 여부를 판단한다. |
예시의 경우 쿠키의 사용을 허용한다. | |
Access-Control-Allow-Headers | 헤더 값에 해당하는 헤더의 사용 가능 여부를 나타냄 |
브라우저는 수신측의 응답이 발신측의 요청과 상응하는지 확인하고, 그때야 비로소 POST 요청을 보내 수신측의 웹 리소스를 요청하는 HTTP 요청을 보냄
이미지나 자바스크립트, CSS등의 리소스는 SOP에 구애 받지 않고 외부 출처에 대해 접근을 허용한다는 사실을 이용해 JSONP 방식은 이러한 특징을 이용해 <script>
태그로 Cross Origin의 데이터를 불러옴
그러나 함수를 활용해야 함
Cross Origin에 요청할 때 callback
파라미터에 어떤 함수로 받아오는 데이터를 핸들링할지 넘겨주면, 대상 서버는 전달된 Callback
으로 데이터를 감싸 응답함
<scirpt>
/* myCallback이라는 콜백 함수를 지정한다. */
function
myCallback(data){
/* 전달받은 인자에서 id를 콘솔에 출력한다. */
console.log(data.id)
}
</script>
<-!--
https://theori.io의 스크립트를 로드하는 HTML 코드임
단, callback이라는 이름의 파라미터를 myCallback으로 지정함으로써
수신측에게 myCallback 함수를 사용해 수신받겠다고 알림.
-->
// Cross Origin의 데이터를 불러옴 + callback 파라미터로 myCallback을 함께 전달함
// Cross Origin에서는 응답 할 데이터를 myCallback으로 감싸 Javascript 코드를 반환해줌
// 반환된 코드는 요청측에서 실행되기 때문에 3~6번 줄에서 정의된 myCallback 함수가 전달된 데이터를 읽을 수 있음
<script src='http://theori.io/whoami?callback=myCallback'></script>
/*
수신측은 myCallback 이라는 함수를 통해 요청측에 데이터를 전달함
전달 할 데이터는 현재 theori.io에서 클라이언트가 사용 중인 계정 정보인
{'id' : 'dreamhack'}임
*/
myCallback({'id' : 'dreamhack'});
다만, JSONP는 CORS가 생기기 전에 사용하던 방법으로 현재는 거의 사용하지 않는 추세이기 때문에, 새롭게 코드를 작성할 때에는 CORS를 사용해야 함
💡 `Same Origin Policy(SOP)` : 동일 출처 정책, 현재 페이지의 출처가 아닌 다른 출처로부터 온 데이터를 읽지 못하게 하는 브라우저의 보안 메커니즘Same Origin
: 현재 페이지와 동일한 출처
Cross Origin
: 현재 페이지와 동일한 출처
Cross Origin Resource Sharing(CORS)
: 교차 출처 리소스 공유, SOP의 제한을 받지 않고 Cross Origin의 데이터를 처리할 수 있도록 해주는 메커니즘