샌드박스 처리된 IFrame에서 안전하게 플레이하기

오늘날의 웹에서 풍부한 환경을 구축할 때 실제로 제어할 수 없는 구성요소와 콘텐츠를 삽입하는 작업은 불가피하게 발생합니다. 서드 파티 위젯은 참여를 유도하고 전반적인 사용자 환경에서 중요한 역할을 할 수 있으며, 사용자 제작 콘텐츠가 사이트의 네이티브 콘텐츠보다 더 중요한 경우도 있습니다. 둘 다 사용하지 않는 것은 실제로 선택사항이 아니지만, 둘 다 사이트에서 무엇인가 문제가 발생할 수 있는 위험을 증가시킵니다. 삽입하는 위젯(모든 광고, 모든 소셜 미디어 위젯)은 악의적인 의도가 있는 사람들에게 잠재적인 공격 벡터가 됩니다.

콘텐츠 보안 정책(CSP)은 특별히 신뢰할 수 있는 스크립트 및 기타 콘텐츠 소스를 허용 목록에 추가하는 기능을 제공하여 이러한 두 가지 유형의 콘텐츠와 관련된 위험을 완화할 수 있습니다. 이는 올바른 방향의 주요 단계이지만 대부분의 CSP 지시어가 제공하는 보호는 바이너리라는 점에 유의해야 합니다. 리소스가 허용되거나 허용되지 않습니다. "이 콘텐츠 소스를 실제로 신뢰할 수는 없지만 정말 예쁘네요. 이걸 삽입해 주세요. 그러나 이로 인해 사이트가 손상되지 않도록 하세요."

최소 권한

본질적으로는 작업을 실행하는 데 필요한 최소한의 기능만 삽입된 콘텐츠에 부여할 수 있는 메커니즘을 찾고 있습니다. 위젯에 새 창을 팝업할 필요가 없으면 window.open 액세스 권한을 제거해도 문제가 없습니다. Flash가 필요하지 않다면 플러그인 지원을 사용 중지해도 문제가 되지 않습니다. 최소 권한의 원칙을 따르고 사용하려는 기능과 직접적인 관련이 없는 모든 기능을 차단하면 최대한 안전할 수 있습니다. 그 결과 삽입된 콘텐츠의 일부가 사용되어서는 안 되는 권한을 활용하지 않을 것이라고 더 이상 맹목적으로 신뢰할 필요가 없습니다. 처음부터 기능에 액세스할 수 없게 됩니다.

iframe 요소는 이러한 솔루션을 위한 좋은 프레임워크로 향하는 첫 번째 단계입니다. iframe에서 신뢰할 수 없는 구성요소를 로드하면 애플리케이션과 로드하려는 콘텐츠를 구분할 수 있습니다. 프레이밍된 콘텐츠는 페이지의 DOM 또는 로컬에 저장한 데이터에 액세스할 수 없으며 페이지의 임의 위치에 그릴 수도 없습니다. 범위는 프레임 윤곽선으로 제한됩니다. 하지만 이 분리는 그다지 강력하지 않습니다. 포함된 페이지에는 여전히 불쾌감을 주거나 악의적인 행동을 할 수 있는 여러 옵션이 있습니다. 자동재생 동영상, 플러그인, 팝업은 빙산의 일각입니다.

iframe 요소의 sandbox 속성은 프레임된 콘텐츠의 제한을 강화하는 데 필요한 기능을 제공합니다. 권한이 낮은 환경에서 특정 프레임의 콘텐츠를 로드하도록 브라우저에 지시하여 작업을 하는 데 필요한 기능의 하위 집합만 허용할 수 있습니다.

틀렸지만 확인

트위터의 '트윗' 버튼은 샌드박스를 통해 사이트에 보다 안전하게 삽입할 수 있는 기능의 좋은 예입니다. Twitter에서는 다음 코드를 사용하여 iframe을 통해 버튼을 삽입할 수 있습니다.

<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
        style="border: 0; width:130px; height:20px;"></iframe>

잠글 수 있는 항목을 파악하기 위해 버튼에 필요한 기능을 주의 깊게 살펴보겠습니다. 프레임에 로드되는 HTML은 Twitter 서버에서 약간의 자바스크립트를 실행하고 클릭 시 트윗 인터페이스가 채워진 팝업을 생성합니다. 해당 인터페이스는 트윗을 올바른 계정에 연결하려면 Twitter 쿠키에 액세스해야 하고 트윗 양식을 제출할 수 있어야 합니다. 여기까지입니다. 프레임이 플러그인을 로드할 필요도 없고, 최상위 창이나 다른 여러 기능을 탐색할 필요도 없습니다. 이러한 권한은 필요하지 않으므로 프레임 콘텐츠를 샌드박싱하여 삭제해 보겠습니다.

샌드박스는 허용 목록을 기반으로 작동합니다. 먼저 가능한 모든 권한을 삭제한 다음 샌드박스 구성에 특정 플래그를 추가하여 개별 기능을 다시 사용 설정합니다. Twitter 위젯의 경우 JavaScript, 팝업, 양식 제출, twitter.com 쿠키를 사용 설정하기로 했습니다. 다음 값을 사용하여 sandbox 속성을 iframe에 추가하면 됩니다.

<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
    src="https://platform.twitter.com/widgets/tweet_button.html"
    style="border: 0; width:130px; height:20px;"></iframe>

이제 모두 완료되었습니다. 프레임에 필요한 모든 기능을 제공했으며, 브라우저는 sandbox 속성 값을 통해 명시적으로 부여하지 않은 모든 권한에 관한 액세스를 거부할 수 있게 해줍니다.

세분화된 기능 제어

위 예에서 가능한 샌드박스 플래그 중 몇 가지를 살펴봤으니 이제 속성의 내부 동작을 좀 더 자세히 살펴보겠습니다.

샌드박스 속성이 비어 있는 iframe이 있는 경우 프레임된 문서는 완전히 샌드박스 처리되어 다음과 같은 제한사항이 적용됩니다.

  • JavaScript는 프레임이 있는 문서에서는 실행되지 않습니다. 여기에는 스크립트 태그를 통해 명시적으로 로드된 자바스크립트뿐만 아니라 인라인 이벤트 핸들러 및 javascript: URL도 포함됩니다. 또한 noscript 태그에 포함된 콘텐츠는 사용자가 스크립트를 직접 사용 중지한 것처럼 표시됩니다.
  • 프레임으로 지정된 문서는 고유한 출처에 로드됩니다. 즉, 모든 동일 출처 검사는 실패합니다. 고유한 출처는 자신뿐만 아니라 다른 출처와도 일치하지 않습니다. 무엇보다도 이는 문서가 출처의 쿠키 또는 다른 스토리지 메커니즘(DOM 스토리지, 색인이 생성된 DB 등)에 저장된 데이터에 액세스할 수 없음을 의미합니다.
  • 프레임된 문서는 window.open 또는 target="_blank" 등을 통해 새 창이나 대화상자를 만들 수 없습니다.
  • 양식을 제출할 수 없습니다.
  • 플러그인이 로드되지 않습니다.
  • 프레임으로 표시된 문서는 최상위 항목이 아닌 자체로만 탐색할 수 있습니다. window.top.location를 설정하면 예외가 발생하고 target="_top"가 포함된 링크를 클릭해도 아무런 효과가 없습니다.
  • 자동으로 트리거되는 기능 (자동 포커스가 설정된 양식 요소, 자동재생 동영상 등)은 차단됩니다.
  • 포인터 잠금을 가져올 수 없습니다.
  • seamless 속성은 프레임된 문서에 포함된 iframes에서 무시됩니다.

이는 상당히 엄격하며 완전히 샌드박스 처리된 iframe에 로드된 문서는 실제로 위험이 거의 없습니다. 물론 그다지 많은 가치를 얻지 못합니다. 정적인 콘텐츠를 위해 전체 샌드박스를 사용할 수도 있지만 대부분의 경우 약간의 느슨함을 덜어야 합니다.

플러그인을 제외하고 이러한 각 제한사항은 샌드박스 속성 값에 플래그를 추가하여 해제할 수 있습니다. 샌드박스 문서는 샌드박스 처리되지 않은 네이티브 코드이므로 플러그인을 실행할 수 없습니다. 하지만 그 외의 모든 것은 정상적인 게임입니다.

  • allow-forms에서는 양식 제출을 허용합니다.
  • allow-popups는 (shock!) 팝업을 허용합니다.
  • allow-pointer-lock는 포인터 잠금을 허용합니다.
  • allow-same-origin를 사용하면 문서에서 출처를 유지할 수 있습니다. https://example.com/에서 로드된 페이지는 출처의 데이터에 계속 액세스할 수 있습니다.
  • allow-scripts는 JavaScript 실행을 허용하고 기능이 자동으로 트리거되도록 합니다 (JavaScript를 통해 구현하기가 쉬움).
  • allow-top-navigation를 사용하면 최상위 창을 탐색하여 문서를 프레임 밖으로 펼칠 수 있습니다.

이를 염두에 두고 위의 Twitter 예에서 특정 샌드박스 플래그 집합을 사용하게 된 이유를 정확히 평가할 수 있습니다.

  • 프레임에 로드된 페이지에서 사용자 상호작용을 처리하기 위해 일부 자바스크립트를 실행하므로 allow-scripts가 필요합니다.
  • 버튼이 새 창에 트윗 양식 팝업으로 표시되므로 allow-popups가 필요합니다.
  • 트윗 양식을 제출할 수 있어야 하므로 allow-forms가 필요합니다.
  • twitter.com의 쿠키에 액세스할 수 없고 사용자가 로그인하여 양식을 게시할 수 없으므로 allow-same-origin가 필요합니다.

한 가지 중요한 점은 프레임에 적용된 샌드박스 플래그는 샌드박스에서 생성된 모든 창이나 프레임에도 적용된다는 것입니다. 즉, 양식이 프레임이 표시되는 창에만 존재하더라도 프레임의 샌드박스에 allow-forms를 추가해야 합니다.

sandbox 속성을 사용하면 위젯은 필요한 권한만 가져오고 플러그인, 상단 탐색, 포인터 잠금과 같은 기능은 차단된 상태로 유지됩니다. 부정적인 영향 없이 위젯을 삽입할 위험을 줄였습니다. 모두에게 이익입니다.

권한 분리

권한이 낮은 환경에서 신뢰할 수 없는 코드를 실행하기 위해 서드 파티 콘텐츠를 샌드박스에 지정하는 것은 상당히 유용합니다. 하지만 여러분의 코드는 어떨까요? 자신을 믿으시죠? 그렇다면 왜 샌드박스에 대해 걱정할까요?

코드에 플러그인이 필요하지 않다면 왜 플러그인에 대한 액세스 권한을 부여합니까? 기껏해야 사용하지 않는 특권이나 최악의 경우 공격자가 침입할 수 있는 잠재적인 벡터입니다. 모든 사람의 코드에는 버그가 있으며 사실상 모든 애플리케이션은 어떤 식으로든 악용에 취약합니다. 자체 코드를 샌드박스화하면 공격자가 애플리케이션을 성공적으로 파괴하더라도 애플리케이션의 출처에 대한 전체 액세스 권한이 부여되지 않으며, 애플리케이션에서 할 수 있는 작업만 수행할 수 있습니다. 여전히 나쁘지만 그렇게 나쁘지는 않습니다.

애플리케이션을 논리적 조각으로 분할하고 가능한 최소 권한으로 각 조각을 샌드박싱하면 위험을 더욱 줄일 수 있습니다. 이 기법은 네이티브 코드에서 매우 일반적입니다. 예를 들어 Chrome은 로컬 하드 드라이브에 액세스하고 네트워크에 연결할 수 있는 높은 권한의 브라우저 프로세스로 나뉘며, 신뢰할 수 없는 콘텐츠를 파싱하는 복잡한 작업을 처리하는 많은 낮은 권한의 렌더러 프로세스도 있습니다. 렌더기는 디스크를 터치할 필요가 없으며, 브라우저는 페이지를 렌더링하는 데 필요한 모든 정보를 제공합니다. 영리한 해커가 렌더러를 손상시킬 방법을 발견하더라도 렌더러는 자체적으로 많은 관심을 끌 수 없으므로 모든 높은 권한 액세스는 브라우저의 프로세스를 통해 라우팅되어야 합니다. 공격자는 피해를 입히기 위해 시스템의 여러 부분에서 여러 개의 구멍을 찾아야 하며, 이를 통해 성공적인 담보 위험을 크게 줄일 수 있습니다.

eval()을(를) 안전하게 샌드박싱합니다.

샌드박스와 postMessage API를 사용하면 이 모델의 성공 여부를 웹에 쉽게 적용할 수 있습니다. 애플리케이션의 일부는 샌드박스 처리된 iframe에 있을 수 있으며 상위 문서는 메시지를 게시하고 응답을 수신 대기하여 애플리케이션 간의 통신을 브로커할 수 있습니다. 이러한 종류의 구조는 앱의 한 부분에서 악용될 경우 가능한 최소한의 피해를 입도록 보장합니다. 또한 명확한 통합 지점을 만들어야 한다는 장점도 있으므로 입력과 출력의 유효성을 검사할 때 주의해야 할 부분을 정확하게 파악할 수 있습니다. 장난감 예시를 통해 어떻게 작동하는지 살펴보겠습니다

Evalbox는 문자열을 가져와 JavaScript로 평가하는 흥미로운 애플리케이션입니다. 와우, 그렇죠? 이 오랜 세월을 기다렸던 자라나는 것이죠. 물론 이 애플리케이션은 매우 위험한 애플리케이션입니다. 임의의 JavaScript가 실행되도록 허용하면 출처에서 제공해야 하는 모든 데이터를 확보할 수 있기 때문입니다. 코드가 샌드박스 내부에서 실행되도록 하여 Bad ThingsTM가 발생할 위험을 완화하여 훨씬 더 안전해집니다. 프레임의 내용부터 시작하여 코드를 처음부터 살펴보겠습니다.

<!-- frame.html -->
<!DOCTYPE html>
<html>
    <head>
    <title>Evalbox's Frame</title>
    <script>
        window.addEventListener('message', function (e) {
        var mainWindow = e.source;
        var result = '';
        try {
            result = eval(e.data);
        } catch (e) {
            result = 'eval() threw an exception.';
        }
        mainWindow.postMessage(result, event.origin);
        });
    </script>
    </head>
</html>

프레임 내부에는 window 객체의 message 이벤트에 연결하여 상위 요소로부터 메시지를 수신 대기하는 최소 문서가 있습니다. 상위 요소가 iframe의 콘텐츠에서 postMessage를 실행할 때마다 이 이벤트가 트리거되어 상위 요소에서 실행하려는 문자열에 액세스할 수 있습니다.

핸들러에서 이벤트의 source 속성(상위 창)을 가져옵니다. 작업이 완료되면 이를 사용하여 힘든 작업 결과를 다시 전송할 것입니다. 그런 다음 주어진 데이터를 eval()에 전달하여 어려운 작업을 실행합니다. 샌드박스 처리된 iframe 내의 금지된 작업은 DOM 예외를 자주 생성하므로 이 호출은 try 블록으로 종료되었습니다. DOM 예외를 포착하고 대신 오류 메시지를 보고합니다. 마지막으로 결과를 상위 창에 다시 게시합니다. 이는 매우 간단한 부분입니다.

상위 요소도 마찬가지로 복잡하지 않습니다. 코드용 textarea와 실행을 위한 button를 사용하여 작은 UI를 만들고 샌드박스 처리된 iframe를 통해 frame.html를 가져와 스크립트 실행만 허용합니다.

<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
        id='sandboxed'
        src='frame.html'></iframe>

이제 실행을 위해 연결할 것입니다. 먼저 iframe의 응답을 수신 대기하고 사용자에게 alert() 응답을 전송합니다. 실제 애플리케이션은 아마도 덜 귀찮은 작업을 할 것입니다.

window.addEventListener('message',
    function (e) {
        // Sandboxed iframes which lack the 'allow-same-origin'
        // header have "null" rather than a valid origin. This means you still
        // have to be careful about accepting data via the messaging API you
        // create. Check that source, and validate those inputs!
        var frame = document.getElementById('sandboxed');
        if (e.origin === "null" &amp;&amp; e.source === frame.contentWindow)
        alert('Result: ' + e.data);
    });

다음으로, 이벤트 핸들러를 연결하여 button를 클릭하도록 합니다. 사용자가 클릭하면 textarea의 현재 콘텐츠를 가져와 실행할 프레임으로 전달합니다.

function evaluate() {
    var frame = document.getElementById('sandboxed');
    var code = document.getElementById('code').value;
    // Note that we're sending the message to "*", rather than some specific
    // origin. Sandboxed iframes which lack the 'allow-same-origin' header
    // don't have an origin which you can target: you'll have to send to any
    // origin, which might alow some esoteric attacks. Validate your output!
    frame.contentWindow.postMessage(code, '*');
}

document.getElementById('safe').addEventListener('click', evaluate);

정말 쉽죠? Google은 매우 간단한 평가 API를 만들었으며, 평가된 코드가 쿠키 또는 DOM 저장소와 같은 민감한 정보에 액세스할 수 없음을 확신할 수 있습니다. 마찬가지로 평가된 코드는 플러그인을 로드하거나, 새 창을 팝업하거나, 기타 성가시게 하거나 악의적인 활동을 할 수 없습니다.

모놀리식 애플리케이션을 단일 목적 구성요소로 분해하여 자체 코드에도 동일한 작업을 수행할 수 있습니다. 각각 위에서 작성한 것과 같이 간단한 메시징 API로 래핑할 수 있습니다. 높은 권한의 상위 창은 컨트롤러 및 디스패처 역할을 할 수 있으며, 각 모듈이 작업을 실행할 수 있는 권한이 가장 적은 특정 모듈로 메시지를 전송하고, 결과를 수신하며, 필요한 정보만 각 모듈에 잘 공급되도록 합니다.

그러나 상위 요소와 동일한 출처에서 프레이밍된 콘텐츠를 처리할 때는 매우 주의해야 합니다. https://example.com/의 페이지가 allow-same-originallow-scripts 플래그가 모두 포함된 샌드박스로 동일한 출처의 다른 페이지를 프레임으로 표시하는 경우 프레임된 페이지가 상위 페이지에 도달하여 샌드박스 속성을 완전히 삭제할 수 있습니다.

샌드박스에서 플레이하기

샌드박스는 현재 이 글을 작성하는 시점 기준으로 Firefox 17 이상, IE10 이상, Chrome 등 다양한 브라우저에서 사용할 수 있습니다 (caniuse의 경우 최신 지원 표가 제공). 포함하는 iframessandbox 속성을 적용하면 콘텐츠가 올바르게 작동하는 데 필요한 권한 표시할 콘텐츠에 대한 특정 권한을 부여할 수 있습니다. 이렇게 하면 콘텐츠 보안 정책으로 이미 가능한 수준 이상으로 서드 파티 콘텐츠 포함과 관련된 위험을 줄일 수 있습니다.

또한 샌드박스는 영리한 공격자가 코드의 허점을 악용할 위험을 줄이는 강력한 기법입니다. 모놀리식 애플리케이션을 일련의 샌드박스 서비스로 분리하면 각각이 소량의 자체 포함된 기능을 담당하므로 공격자는 특정 프레임의 콘텐츠뿐만 아니라 컨트롤러도 손상시켜야 합니다. 이는 특히 컨트롤러의 범위를 크게 줄일 수 있기 때문에 훨씬 더 어려운 작업입니다. 브라우저에 나머지 작업을 요청하면 해당 코드를 감사하는 데 보안과 관련된 노력을 기울일 수 있습니다.

샌드박스가 인터넷 보안 문제를 완벽하게 해결할 수 있는 것은 아닙니다. 심층 방어를 제공하며 사용자의 클라이언트를 제어할 수 없다면 아직 모든 사용자를 위해 브라우저 지원에 의존할 수 없습니다(사용자 클라이언트를 제어하는 경우(예: 엔터프라이즈 환경)). 언젠가는 샌드박스 생성이 방어 체계를 강화하기 위한 또 다른 보호 단계이지만 전적으로 의존할 수 있는 완전한 방어 체계는 아닙니다. 그래도 레이어는 훌륭합니다. 이걸 활용해 보세요

추가 자료

  • 'HTML5 애플리케이션의 권한 분리'는 작은 프레임워크를 설계하고 이를 기존 HTML5 앱 3개에 적용하는 과정을 다룬 흥미로운 자료입니다.

  • 샌드박스는 다른 두 가지 새로운 iframe 속성인 srcdocseamless과 결합할 때 훨씬 더 유연할 수 있습니다. 전자를 사용하면 HTTP 요청의 오버헤드 없이 콘텐츠로 프레임을 채울 수 있고, 후자를 사용하면 스타일이 프레임된 콘텐츠에 흐르도록 할 수 있습니다. 둘 다 현재 꽤 비효율적인 브라우저 지원 (Chrome 및 WebKit Nightlies)을 제공하지만 향후 흥미로운 조합이 될 것입니다. 예를 들어 다음 코드를 통해 기사의 샌드박스 댓글을 작성할 수 있습니다.

        <iframe sandbox seamless
                srcdoc="<p>This is a user's comment!
                           It can't execute script!
                           Hooray for safety!</p>"></iframe>