오늘날의 웹에서 풍부한 경험을 구축하려면 거의 필연적으로 개발자가 실제로 제어할 수 없는 구성요소와 콘텐츠를 삽입해야 합니다. 서드 파티 위젯은 참여를 유도하고 전반적인 사용자 환경에서 중요한 역할을 할 수 있으며, 사용자 제작 콘텐츠는 사이트의 기본 콘텐츠보다 더 중요할 수도 있습니다. 둘 중 하나를 포기하는 것은 선택사항이 아니지만, 둘 다 사이트에서 Something Bad™가 발생할 위험을 높입니다. 삽입하는 각 위젯(모든 광고, 모든 소셜 미디어 위젯)은 악의적인 의도를 가진 사람들에게 잠재적인 공격 벡터입니다.
콘텐츠 보안 정책(CSP)은 스크립트 및 기타 콘텐츠의 신뢰할 수 있는 소스를 허용 목록에 추가할 수 있는 기능을 제공하여 이러한 두 가지 유형의 콘텐츠와 관련된 위험을 완화할 수 있습니다. 이는 올바른 방향으로 나아가는 중요한 단계이지만 대부분의 CSP 지시문이 제공하는 보호는 바이너리라는 점에 유의해야 합니다. 즉, 리소스가 허용되거나 허용되지 않습니다. "이 콘텐츠 소스를 실제로 신뢰할 수 있는지 확실하지 않지만, 너무 예뻐요! 브라우저에 삽입하되 사이트가 깨지지 않게 해 줘."
최소 권한
기본적으로 Google은 삽입하는 콘텐츠에 작업을 수행하는 데 필요한 최소 수준의 기능만 부여할 수 있는 메커니즘을 찾고 있습니다. 위젯이 새 창을 표시할 필요가 없는 경우 window.open에 대한 액세스 권한을 제거해도 문제가 되지 않습니다. Flash가 필요하지 않은 경우 플러그인 지원을 사용 중지해도 문제가 되지 않습니다. 최소 권한의 원칙을 따르고 사용하려는 기능과 직접 관련이 없는 모든 기능을 차단하면 최대한 안전하게 보호할 수 있습니다. 그 결과, 일부 삽입된 콘텐츠가 사용해서는 안 되는 권한을 이용하지 않을 것이라고 맹목적으로 신뢰할 필요가 없어졌습니다. 애초에 기능에 액세스할 수 없습니다.
iframe 요소는 이러한 솔루션을 위한 좋은 프레임워크를 구축하는 첫 번째 단계입니다.
iframe에 신뢰할 수 없는 구성요소를 로드하면 애플리케이션과 로드하려는 콘텐츠가 어느 정도 분리됩니다. 프레임 콘텐츠는 페이지의 DOM 또는 로컬에 저장된 데이터에 액세스할 수 없으며 페이지의 임의 위치에 그릴 수도 없습니다. 범위는 프레임의 윤곽선으로 제한됩니다. 하지만 분리가 완전히 강력하지는 않습니다. 포함된 페이지에는 여전히 성가시거나 악의적인 동작을 위한 여러 옵션이 있습니다. 자동 재생 동영상, 플러그인, 팝업은 빙산의 일각에 불과합니다.
iframe 요소의 sandbox 속성
은 프레임 콘텐츠에 대한 제한을 강화하는 데 필요한 기능을 제공합니다. 브라우저가 권한이 낮은 환경에서 특정 프레임의 콘텐츠를 로드하도록 지시하여 필요한 작업을 수행하는 데 필요한 기능의 하위 집합만 허용할 수 있습니다.
신뢰하되 검증하기
Twitter의 '트윗' 버튼은 샌드박스를 통해 사이트에 더 안전하게 삽입할 수 있는 기능의 좋은 예입니다. Twitter에서는 다음 코드를 사용하여 iframe을 통해 버튼을 삽입할 수 있습니다.
<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
style="border: 0; width:130px; height:20px;"></iframe>
잠글 수 있는 항목을 파악하기 위해 버튼에 필요한 기능을 자세히 살펴보겠습니다. 프레임에 로드된 HTML은 Twitter 서버에서 약간의 JavaScript를 실행하고 클릭 시 트윗 인터페이스로 채워진 팝업을 생성합니다. 이 인터페이스는 트윗을 올바른 계정에 연결하기 위해 트위터의 쿠키에 액세스해야 하며 트윗 양식을 제출할 수 있어야 합니다. 이것이 전부입니다. 프레임은 플러그인을 로드할 필요가 없고, 최상위 창을 탐색할 필요가 없으며, 다른 여러 기능을 수행할 필요가 없습니다. 이러한 권한은 필요하지 않으므로 프레임의 콘텐츠를 샌드박스 처리하여 삭제해 보겠습니다.
샌드박싱은 허용 목록을 기반으로 작동합니다. 먼저 가능한 모든 권한을 삭제한 다음 샌드박스 구성에 특정 플래그를 추가하여 개별 기능을 다시 사용 설정합니다. 트위터 위젯의 경우 자바스크립트, 팝업, 양식 제출, twitter.com의 쿠키를 사용 설정하기로 결정했습니다. 다음 값을 사용하여 iframe에 sandbox 속성을 추가하면 됩니다.
<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뿐만 아니라 인라인 이벤트 핸들러와 javascript: URL도 포함됩니다. 또한 noscript 태그에 포함된 콘텐츠가 사용자가 직접 스크립트를 사용 중지한 것처럼 표시됩니다.
- 프레임 문서가 고유한 출처에 로드됩니다. 즉, 모든 동일 출처 검사가 실패합니다. 고유한 출처는 다른 출처와 일치하지 않으며, 심지어 자체와도 일치하지 않습니다. 다른 영향 중에서도 이는 문서가 출처의 쿠키 또는 기타 저장 메커니즘(DOM 저장소, Indexed DB 등)에 저장된 데이터에 액세스할 수 없음을 의미합니다.
- 프레임 문서에서 새 창이나 대화상자를 만들 수 없습니다 (예:
window.open또는target="_blank"를 통해). - 양식을 제출할 수 없습니다.
- 플러그인이 로드되지 않습니다.
- 프레임 문서가 최상위 상위 요소가 아닌 자체만 탐색할 수 있습니다.
window.top.location를 설정하면 예외가 발생하고target="_top"가 있는 링크를 클릭해도 아무런 효과가 없습니다. - 자동으로 트리거되는 기능 (자동 포커스 양식 요소, 자동 재생 동영상 등)이 차단됩니다.
- 포인터 잠금을 가져올 수 없습니다.
- 프레임 문서에 포함된
iframes에서seamless속성이 무시됩니다.
이는 매우 엄격한 조치이며, 완전히 샌드박스 처리된 iframe에 로드된 문서는 실제로 위험이 거의 없습니다. 물론 가치 있는 작업도 많이 할 수 없습니다. 일부 정적 콘텐츠의 경우 전체 샌드박스를 사용할 수 있지만 대부분의 경우 약간 완화하는 것이 좋습니다.
플러그인을 제외하고 이러한 각 제한사항은 샌드박스 속성 값에 플래그를 추가하여 해제할 수 있습니다. 샌드박스 처리된 문서는 플러그인을 실행할 수 없습니다. 플러그인은 샌드박스 처리되지 않은 네이티브 코드이기 때문입니다. 하지만 그 외의 모든 항목은 공정한 게임입니다.
allow-forms는 양식 제출을 허용합니다.allow-popups는 팝업을 허용합니다.allow-pointer-lock는 포인터 잠금을 허용합니다.allow-same-origin를 사용하면 문서가 출처를 유지할 수 있습니다.https://example.com/에서 로드된 페이지는 해당 출처의 데이터에 대한 액세스 권한을 유지합니다.allow-scripts는 JavaScript 실행을 허용하고 기능이 자동으로 트리거되도록 허용합니다 (JavaScript를 통해 구현하기가 간단하기 때문).allow-top-navigation는 문서가 최상위 창을 탐색하여 프레임에서 벗어날 수 있도록 허용합니다.
이를 염두에 두고 위의 Twitter 예시에서 특정 샌드박스 플래그 집합을 사용하게 된 이유를 정확하게 평가할 수 있습니다.
- 프레임에 로드된 페이지가 사용자 상호작용을 처리하기 위해 일부 JavaScript를 실행하므로
allow-scripts가 필요합니다. - 버튼이 새 창에 트윗 양식을 표시하므로
allow-popups가 필요합니다. - 트윗 양식을 제출할 수 있어야 하므로
allow-forms가 필요합니다. allow-same-origin는 필수입니다. 그렇지 않으면 twitter.com의 쿠키에 액세스할 수 없으며 사용자가 로그인하여 양식을 게시할 수 없습니다.
한 가지 중요한 점은 프레임에 적용된 샌드박스 플래그가 샌드박스에서 생성된 모든 창이나 프레임에도 적용된다는 것입니다. 즉, 양식이 프레임이 표시되는 창에만 존재하더라도 프레임의 샌드박스에 allow-forms를 추가해야 합니다.
sandbox 속성이 있으면 위젯은 필요한 권한만 가져오고 플러그인, 상단 탐색, 포인터 잠금과 같은 기능은 차단된 상태로 유지됩니다. 위젯 삽입의 위험을 줄였으며 부작용은 없습니다.
모두에게 이익이 되는 혜택입니다.
권한 분리
권한이 낮은 환경에서 신뢰할 수 없는 코드를 실행하기 위해 서드 파티 콘텐츠를 샌드박스 처리하는 것은 분명히 유용합니다. 하지만 자체 코드는 어떨까요? 자신을 믿으시죠? 그렇다면 샌드박스에 대해 걱정할 필요가 있을까요?
이 질문을 뒤집어 보겠습니다. 코드에 플러그인이 필요하지 않다면 왜 플러그인에 대한 액세스 권한을 부여하나요? 잘해야 사용하지 않는 권한이고, 최악의 경우 공격자가 발판을 마련할 수 있는 잠재적인 벡터입니다. 모든 사람의 코드에는 버그가 있으며, 실제로 모든 애플리케이션은 어떤 방식으로든 악용에 취약합니다. 자체 코드를 샌드박스 처리하면 공격자가 애플리케이션을 전복하는 데 성공하더라도 애플리케이션의 출처에 대한 전체 액세스 권한이 부여되지 않습니다. 애플리케이션이 할 수 있는 작업만 할 수 있습니다. 여전히 나쁘지만 최악은 아닙니다.
애플리케이션을 논리적 조각으로 나누고 가능한 최소 권한으로 각 조각을 샌드박스 처리하면 위험을 더욱 줄일 수 있습니다. 이 기법은 네이티브 코드에서 매우 일반적입니다. 예를 들어 Chrome은 로컬 하드 드라이브에 액세스할 수 있고 네트워크 연결을 만들 수 있는 높은 권한의 브라우저 프로세스와 신뢰할 수 없는 콘텐츠를 파싱하는 무거운 작업을 수행하는 많은 낮은 권한의 렌더러 프로세스로 분할됩니다. 렌더러는 디스크를 터치할 필요가 없으며, 브라우저는 페이지를 렌더링하는 데 필요한 모든 정보를 렌더러에 제공합니다. 영리한 해커가 렌더러를 손상시키는 방법을 찾더라도 렌더러는 자체적으로 많은 작업을 수행할 수 없으므로 큰 성과를 거두지 못합니다. 모든 높은 권한 액세스는 브라우저의 프로세스를 통해 라우팅되어야 합니다. 공격자는 피해를 입히기 위해 시스템의 여러 부분에서 여러 허점을 찾아야 하므로 성공적인 pwnage의 위험이 크게 줄어듭니다.
eval() 안전하게 샌드박싱
샌드박스 및 postMessage API를 사용하면 이 모델의 성공을 웹에 적용하는 것이 매우 간단합니다. 애플리케이션의 일부는 샌드박스 처리된 iframe에 있을 수 있으며 상위 문서는 메시지를 게시하고 응답을 수신 대기하여 이들 간의 통신을 중개할 수 있습니다. 이러한 구조는 앱의 한 부분에서 발생하는 악용이 최소한의 피해를 입히도록 합니다. 또한 명확한 통합 지점을 만들어야 하므로 입력 및 출력 검증에 주의해야 하는 위치를 정확히 알 수 있다는 장점도 있습니다. 간단한 예를 통해
어떻게 작동하는지 살펴보겠습니다
Evalbox는 문자열을 가져와 JavaScript로 평가하는 흥미로운 애플리케이션입니다. 놀랍지 않나요? 오랫동안 기다려 온 바로 그 소식입니다. 물론 임의의 JavaScript를 실행하도록 허용하면 출처에서 제공하는 모든 데이터를 가져올 수 있으므로 상당히 위험한 애플리케이션입니다. 코드가 샌드박스 내에서 실행되도록 하여 Bad Things™가 발생할 위험을 완화하면 훨씬 더 안전해집니다. 프레임의 콘텐츠부터 시작하여 안에서 밖으로 코드를 살펴보겠습니다.
<!-- 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 블록에 래핑되었습니다. 이러한 예외를 포착하여 친숙한 오류 메시지를 대신 보고합니다. 마지막으로 결과를 상위 창에 다시 게시합니다. 이건 꽤 간단한 작업입니다.
상위 요소도 마찬가지로 간단합니다. 코드용 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" && 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);
정말 쉽죠? 매우 간단한 평가 API를 만들었으며 평가된 코드가 쿠키나 DOM 저장소와 같은 민감한 정보에 액세스할 수 없음을 확신할 수 있습니다. 마찬가지로 평가된 코드는 플러그인을 로드하거나 새 창을 표시하거나 기타 여러 성가시거나 악의적인 활동을 할 수 없습니다.
모놀리식 애플리케이션을 단일 목적 구성요소로 분할하여 자체 코드에 대해서도 동일한 작업을 수행할 수 있습니다. 각각은 위에서 작성한 것과 같이 간단한 메시지 API로 래핑할 수 있습니다. 권한이 높은 상위 창은 컨트롤러 및 디스패처 역할을 하여 작업을 수행하는 데 필요한 최소한의 권한을 가진 특정 모듈에 메시지를 전송하고 결과를 수신 대기하며 각 모듈에 필요한 정보만 제공되도록 할 수 있습니다.
하지만 상위 요소와 동일한 출처에서 가져온 프레임 콘텐츠를 처리할 때는 매우 주의해야 합니다. https://example.com/의 페이지가 allow-same-origin 및 allow-scripts 플래그를 모두 포함하는 샌드박스를 사용하여 동일한 출처의 다른 페이지를 프레이밍하는 경우 프레이밍된 페이지가 상위 요소에 도달하여 샌드박스 속성을 완전히 삭제할 수 있습니다.
샌드박스에서 플레이하기
샌드박스는 현재 다양한 브라우저에서 사용할 수 있습니다. 이 글을 작성하는 시점에서는 Firefox 17+, IE10+, Chrome에서 사용할 수 있습니다 (물론 caniuse에는 최신 지원 표가 있습니다). 포함하는 iframes에 sandbox 속성을 적용하면 표시되는 콘텐츠에 특정 권한을 부여할 수 있으며, 콘텐츠가 올바르게 작동하는 데 필요한 권한만 부여할 수 있습니다. 이를 통해 콘텐츠 보안 정책으로 이미 가능한 것 이상으로 서드 파티 콘텐츠 포함과 관련된 위험을 줄일 수 있습니다.
또한 샌드박싱은 영리한 공격자가 코드의 허점을 악용할 수 있는 위험을 줄이는 강력한 기법입니다. 모놀리식 애플리케이션을 각각 독립적인 기능의 작은 부분을 담당하는 샌드박스 처리된 서비스 집합으로 분리하면 공격자는 특정 프레임의 콘텐츠뿐만 아니라 컨트롤러도 손상시켜야 합니다. 특히 컨트롤러의 범위를 크게 줄일 수 있으므로 훨씬 더 어려운 작업입니다. 브라우저에 나머지 코드에 대한 도움을 요청하면 해당 코드를 감사하는 데 보안 관련 노력을 기울일 수 있습니다.
샌드박스가 인터넷 보안 문제의 완전한 해결책이라는 의미는 아닙니다. 심층 방어를 제공하며, 사용자의 클라이언트를 제어할 수 없는 경우 아직 모든 사용자에 대한 브라우저 지원에 의존할 수 없습니다(사용자의 클라이언트를 제어하는 경우(예: 엔터프라이즈 환경)는 예외). 언젠가는 가능할 수도 있지만, 현재로서는 샌드박스 처리는 방어력을 강화하는 또 다른 보호 계층일 뿐이며, 전적으로 의존할 수 있는 완전한 방어 수단은 아닙니다. 그래도 레이어는 훌륭합니다. 이 기능을 사용하는 것이 좋습니다.
추가 자료
'HTML5 애플리케이션의 권한 분리'는 소규모 프레임워크의 설계와 기존 HTML5 앱 3개에 대한 적용을 다루는 흥미로운 논문입니다.
샌드박스 처리는 다른 두 가지 새로운 iframe 속성인
srcdoc및seamless와 결합하면 훨씬 더 유연해질 수 있습니다. 전자를 사용하면 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>