웹 브라우저 보안, Same Origin Policy

브라우저의 보안 정책인 SOP에 대해서 알아보고, CSRF라는 공격 방법에 대해 알아봅니다. 또한 다른 출처로 Ajax 요청을 보내기 위한 JSONP와 SOP을 제어할 수 있는 CORS에 대해서 알아봅니다.
CSRF, CORS, SOP, JSONP, 웹 보안


SOP

Same Origin PolicySame Origin Policy

동일 출처 정책(Same Origin Policy)은 웹 브라우저의 핵심적인 보안 모델입니다.

  • http://workshop.benzen.io/api/exit 라는 URL로 Request를 보내면 워크샵 계정을 탈퇴시킨다고 해봅시다.
  • 물론 이때 워크샵 웹 서버는 세션을 통해서 인증 절차를 거칩니다.
  • 한 유저가 웹 브라우저에서 http://workshop.benzen.io에 로그인된 상태로, 다시 http://www.hacking.com로 접속했습니다.
  • 이 때 hacking.com에서 응답해준 HTML 문서에서 아래처럼 Ajax를 통해 Request를 보낸다면, 유저는 생각도 못한 새에 계정을 탈퇴당하고 맙니다.
<script>$.get("http://worshop.benzen.io/api/exit");</script>

이와 같은 문제를 막고자 웹 브라우저들은 동일 출처 정책을 기본적으로 따르고 있습니다. 동일 출처 정책에 따라 A 출처에서 받은 문서는 A 출처에서 받은 다른 문서에 접근하고 제어 할 수 있으나, B 출처에서 받은 문서에 접근하거나 제어 할 수 없습니다.
좀 더 구체적으로 생각해보면

  • A 출처에서 받은 HTML에는 B 출처의 JavaScript나 CSS 파일이 적용되지 않는다?
  • A 출처에서 받은 HTML에서 B 출처의 이미지 파일을 불러 올 수 없다?

위처럼 된다면 상당히 불편하겠죠. 실제로는 자원의 종류에 따라서 SOP가 다르게 적용됩니다.

  • HTML 문서에서 다른 출처로 폼 제출(GET/POST Request), 링크 클릭(GET Request) 등은 허용됩니다.
  • HTML 문서에서 다른 출처의 CSS, JavaScript, Iframe, Image, Media 등을 포함(GET Request)하는 것은 허용됩니다.
  • HTML 문서에서 다른 출처로 XHR (Ajax Request)을 보내는 것은 거부됩니다.

이외에도 정책에 미묘한 차이점들이 있지만 가장 주요한 이슈는 다른 출처의 대부분의 자원에 대해서 GET Request는 허용되며, Ajax Request는 거부된다는 점입니다.

출처 (Origin)

출처는 프로토콜, 도메인(호스트), 포트를 모두 포함합니다. 즉 동일 출처(Same Origin)이려면 셋 모두가 동일해야합니다. 그렇지 않을 때는 교차 출처(Cross Origin)로 판단되어 SOP가 적용됩니다.

CSRF

CSRFCSRF

위에서 예로 든 워크샵 계정 탈퇴 같은 공격을 사이트 간 요청 위조(Cross-site Request Forgery)라고 합니다. 다른 출처로의 Ajax 요청은 SOP에 의해 거부되더라도, 이미지 태그 등을 통해서 GET Request를 위조 할 수 있습니다.

GET 메소드와 Side-Effect

<img src="http://workshop.benzen.io/api/exit">

이렇게 위조된 GET 요청들은 어떻게 막아야 할까요? HTTP 프로토콜 스펙에서는 GET 메소드를 통해서는 서버의 데이터를 변경하는 부가 작용(Side-Effect)에 대해 라우팅하지 않기를 권고하고 있습니다. 애초에 웹 서버에서 탈퇴를 처리하는 라우팅을 POST 메소드 등으로 설정하는 것이 바람직하겠습니다.

POST 메소드와 Anti-CSRF Token

<h1>You Idiot!</h1>
<form name="hack" method="post" action="http://workshop.benzen.io/api/exit"></form>
<script>document.forms.hack.submit();</script>

그래도 아직 한가지 문제가 있습니다. 폼 제출의 경우는 다른 출처로의 POST 메소드 요청이 허용됩니다. 간단하게는 POST 메소드를 받는 라우팅에서 Referer 헤더를 확인하여 출처를 비교하면 타 출처에서 위조된 요청을 막을 수 있습니다. 다만 Referer 헤더 역시 조작이 가능하고, Referer 헤더가 없는 경우도 있을 수 있기 때문에 완벽하지는 않습니다.

Referer 헤더는 요청을 생성된 웹페이지의 URL을 명시합니다.

확실한 보안을 위해서 Anti-CSRF Token이라는 보안 요소를 도입합니다. 토큰이라함은 무작위로 생성된 문자열을 의미합니다.

Anti-CSRF Token의 구현

  • 서버가 폼을 가진 HTML을 응답해줄 때, 매번 새로운 토큰을 생성하고 세션에 기록해둡니다.
  • 이때 HTML의 폼에 토큰을 hidden 타입으로 추가 합니다.
  • 이후에 서버는 POST 요청을 받은 경우 폼 데이터에 첨부된 토큰을 마지막에 생성해준 토큰과 비교합니다.

Anti-CSRF Token (쿠키를 이용하는 방식)Anti-CSRF Token (쿠키를 이용하는 방식)

요약

위에서 다룬 중요한 보안요소들을 정리하자면 다음과 같습니다.

  • GET 메소드의 라우팅에는 사이드 이펙트가 없도록 한다.
  • POST 메소드의 라우팅에 Anti-CSRF Token을 사용 할 수 있다.

JSONP

JSONPJSONP

Ajax를 통해 다른 출처의 API를 이용하고 싶어도 SOP 때문에 불가능한 경우가 있습니다. 이 때는 JSONP(JSON with Padding)라는 편법을 쓸 수 있습니다. 원리는 <script src="http://other.site/some/path"></script>는 SOP의 제한을 받지 않는데 있습니다.

JSONP의 원리

<script>
function otherSiteCallback(data){
    // ... 다른 사이트에서 데이터를 받으면 하고 싶은 처리를 명시
    console.log(data);
}
</script>
<script src="http://othersite.com/some/path?callback=otherSiteCallback"></script>

othersite.com에서는 /some/path로 GET Request를 받으면 동적으로

otherSiteCallback([
    // server data...,
    // server data...,
    // server data...,
])

의 내용을 가진 스크립트(Content-Type: application/javascript)를 생성해서 Response로 줍니다. 물론 서버에서 otherSiteCallback 같은 콜백의 이름을 요청에 따라 생성하지 않고, jsonCallback 등으로 고정해 놓을 수도 있습니다. 이 소통 방식을 JSONP라고 합니다.

JSONP의 추상화

function callJSONP(url, callbackName, callback) {
    // 웹 브라우저에 미리 함수 등록
    window[callbackName] = callback;

    // script 태그 생성
    var script = document.createElement("script");
    script.src = url+callbackName;
    document.body.appendChild(script);
}

// 이후 아래처럼 사용 할 수 있습니다.
callJSONP('http://othersite.com/some/path?callback=', 'otherSiteCallback', function(data){
    // ... 다른 사이트에서 데이터를 받으면 하고 싶은 처리를 명시
    console.log(data);
});

CORS

CORSCORS

SOP는 웹 브라우저에서 많은 보안 문제를 해결해주지만, 그와 함께 JSONP와 같은 희한한 소통 방식을 만들어 내기도 했습니다. 교차 출처 자원 공유, CORS(Cross Origin Resource Sharing)는 SOP를 원칙적으로 제어 할 수 있는 HTTP 프로토콜의 메커니즘입니다. 교차 출처 간의 XHR처럼 SOP를 위배하는 Request가 발생 할 경우 실제 웹 브라우저의 Request는 아래 그림과 같이 발생합니다.

Preflight Request (OPTION)Preflight Request (OPTION)

Request가 일어나기 전에 CORS의 가능 여부를 물어보는 Preflighed RequestOPTIONS 메소드로 선행되고, 웹 서버가 CORS를 승낙한다고 응답해주면, 이후 정상적인 Reqeust, Response가 교환됩니다. 자사의 API를 공개하고자 하는 웹 서비스에서는 CORS를 지원하여 API 사용자들이 Ajax를 이용해 개발을 할 수 있도록 지원 할 수 있겠습니다.

HTTP Access-Control 헤더 (CORS 헤더)
CORS는 XHR만을 위한 메커니즘은 아닙니다.

모든 출처에 대해 CORS를 지원하는 서버

const app = require('express')();

app.use((req, res, next) => {
    if (req.headers.origin) {
        res.set({
            'Access-Control-Allow-Origin': req.headers.origin,
            'Access-Control-Allow-Methods': 'POST, GET, PUT, DELETE, OPTIONS'
        });

        if (req.method == 'options')
            res.end();

    }
    
    next();
});

app.get('/', (req,res)=>{
    res.end('hey');
});

app.listen(8787);
목차
4. 웹 백엔드
5. 데이터베이스
저자

김동욱

개발 경력 약 10년, 전문 분야는 웹 및 각종 응용 소프트웨어 플랫폼입니다. Codeflow를 운영하고 있습니다.

2018년 04월 10일 업데이트

지원되지 않는 웹 브라우저거나 예기치 않은 오류가 발생했습니다.