(번역) 자바스크립트에서 영역(realm)이란 무엇인가요?

sehyun hwang·2022년 11월 26일
33

FE 번역글

목록 보기
14/29
post-thumbnail

원문 : https://weizman.github.io/page-what-is-a-realm-in-js/

2022/10/28

브라우저 자바스크립트 보안에 관한 장기적인 연구의 일부로서, 저는 지난 몇 년간 영역(realm)에 대한 보안에 집중해왔습니다⭐.

의존성 기반 개발의 부상으로 자바스크립트 생태계(특히 브라우저 자바스크립트 생태계)는 우리가 흔히 "공급망 공격(supply chain attack)"으로 알고 있는 것에 더욱 취약해졌습니다. 그리고 자바스크립트에 새로운 영역을 생성하는 기능은 웹 앱에서 이러한 공격을 성공적으로 대응하기 위해 활용되고 있습니다. (그 이유에 대해 이해하고 싶다면 이전 글을 읽어볼 것을 추천해 드립니다.)

영역 보안 분야는 지금까지 제대로 다뤄지지 않았으며, 저는 최초의 오픈 소스 영역 보안 툴인 LavaMoat🌋Snow-JS❄️를 도입함으로써 이 문제가 점차 개선되기를 희망합니다. (계속 지켜봐주세요.)

하지만 그 전에 우리는 우선 영역이 무엇인지를 이해해야 합니다. 그리고 이 질문에 대한 대답을 정확하면서도 일상적인 언어로 교육적인 방식을 통해 설명하는 것은 결코 쉬운 일이 아닙니다.

이 글의 맥락은 브라우저에서의 자바스크립트에 초점이 맞춰져 있습니다. 따라서 일반적인 자바스크립트에도 적용될 수 있지만, 이를 보장할 수는 없습니다.

영역(realm) - 자바스크립트가 사는 세상

일상에 빗대어 표현하자면, 영역은 기본적으로 자바스크립트 프로그램이 사는 생태계라고 생각해볼 수 있습니다. 그리고 여느 생태계처럼 자바스크립트 프로그램이 존재하기 위해 반드시 갖추어야 할 다른 요소도 함께 포함합니다.

그러면 자바스크립트 프로그램이 필요한 것은 무엇일까요?

1) 전역 실행 환경

자바스크립트에서는 같은 환경 내에서 여러 다른 스크립트가 동작할 수 있습니다. 스크립트는 값(value)과 표현식(expression)이 "보이거나" 참조될 수 있는 표준 실행 환경인 스코프(scope)를 형성할 수 있습니다. 자식 스코프에서 부모 스코프에 접근할 수 있도록 스코프를 계층화할 수 있지만 그 반대는 불가합니다.

<script>
    (function scope1() {
        const x = 1;
        (function scope2() {
            const y = x + 2; // 3
        }());
        const z = x + y;  // Uncaught ReferenceError: y is not defined
    }());
</script>

위의 예제를 통해 자바스크립트 스코프가 어떻게 정의되는지 확인할 수 있습니다. 그러나 실제로 스코프를 선언하지 않고, 변수를 선언하는 자바스크립트 프로그램을 작성한다면 어떻게 될까요?

이는 "최상위 선언(top level declaration)"으로 알려져 있습니다. 정의된 스코프 외부에 선언되는 (또는 일반적으로 실행되는) 모든 것은 가장 바깥의 기본 스코프인 전역 실행 환경내에 위치하게 됩니다.

가장 바깥 스코프에 선언된 변수들은 전역 실행 환경 내에서 서로 다른 스크립트 간에 공유됩니다.

<script> const x = 1; </script>
<script> const y = 2; </script>
<script> const z = x + y; </script> // 3

영역은 자바스크립트 프로그램에 자체 단일 전역 실행 환경을 제공합니다.

위의 예제는 let, class, module, import, 및/또는 function 선언과 함께 "선언적 환경(declarative environment)"으로 알려진 새로운 정의를 구성하는 const를 사용하고 있습니다.

새로운 정의를 생성하는 또 다른 방법들은 "객체 환경(object environment)"으로 분류되며, 여기에는 var, function, async function, function *, async function* (*는 제너레이터 함수를 나타냄)가 포함됩니다.

자바스크립트 코드가 use strict;내에서 실행되거나 또는 스크립트 코드가 아닌 모듈 코드로서 실행될 때 선언문이 "객체 환경"내에서 전역 객체에 미치는 영향은 위의 설명과 다르다는 것을 명심하세요!

"선언적 환경"과 "객체 환경"은 함께 앞서 언급한 전역 실행 환경을 구성합니다.

위에서 언급한 것 외에도 "객체 환경"은 "전역 객체"로 알려진 기본 객체를 통해 "내장 전역 객체"로 알려진 것들을 모두 제공합니다.

2) 전역 객체 (그리고 고유 객체)

자바스크립트 프로그램이 실행될 수 있는 적절한 환경을 가진 이후에, 플랫폼 기반 작업을 포함하되 이에 국한되지 않는 고급 작업을 수행할 수 있어야 합니다.

전역 객체는 고유 객체, 객체, API 등 (플랫폼이 특정되었는지에 상관없이) 더 많은 기능을 갖추고 유용하게 만드는 빌트인에 대한 접근을 허용합니다.

브라우저에서는 window, NodeJS 환경에서는 global로 전역 객체에 접근할 수 있으며 globalThis로 양쪽 환경 모두에서 사용할 수 있습니다.

플랫폼과 관련 없는 것부터 시작하자면, 전역 객체는 몇 가지 내장 고유 객체를 제공합니다.

  1. values (ex. undefined, Infinity 등)
  2. functions (ex. eval, parseInt 등)
  3. constructors (ex. Boolean, Date 등)
  4. others (ex. JSON, Math 등)

이에 더하여 전역 객체는 서로 다른 플랫폼 별 API도 제공합니다. 예를 들어, 브라우저에서는 fetch, alert, document 등이 있습니다.

예시로 DOM은 전역 객체를 통해 노출되는 잘 알려진 브라우저 전용 API입니다. 그리고 여기에서도 모든 영역은 고유한 별도 DOM을 갖습니다.

"전역 실행 환경" 섹션의 맥락에서 전역 객체는 이러한 빌트인에 더불어 "객체 환경"내에 선언된 모든 것을 노출합니다.

// `const` 선언은 "선언적 환경"으로 분류됩니다.
const constant = 1;

// 따라서 전역 객체를 통해 접근할 수 없습니다.
console.log(window.constant); // undefined

// 그러나,

// `var` 선언은 "객체 환경"으로 분류됩니다.
var variable = 2;

// 따라서 전역 객체를 통해 접근할 수 있습니다.
console.log(window.variable); // 2

플랫폼 기반의 객체와 API는 모든 고유 객체 그리고 코드에 선언된 새로운 속성과 함께 전역 객체를 통해 접근할 수 있습니다.

3) 자바스크립트 그 자체

영역과 관련된 마지막 항목은 해당 영역의 실행 환경 내에서 실행되는 자바스크립트 코드입니다.

실행 환경, 전역 객체 또는 영역 내에서 파생된 항목에 대한 모든 변경, 교체, 업데이트는 영역과 독점적으로 관련이 있습니다.

영역이 실제로 무엇인지에 대한 개념 파악하기

지루한 기술 정의 부분을 통과하신 것을 축하합니다 🎉 지금부터는 클릭해볼 수 있는 덜 형식적인 부분입니다.

"현실 세계"에서의 영역

앞서 말했듯이 영역은 자바스크립트 개념으로 브라우저에만 국한된 것이 아닙니다. 하지만 제 설명에서는 브라우저를 기준으로 합니다.

이제 영역이 무엇인지 정의했으니, "실제로 만나볼" 차례입니다.

브라우저에는 기본적으로 단 하나의 영역만 존재하며 이는 메인 영역입니다.

우리가 앞서 배운대로 웹 앱은 서로 다른 고유 객체와 플랫폼 별 API 등에 접근이 가능한 전역 객체와 가장 바깥의 스코프인 전역 실행 환경을 제공하는 영역에 위치합니다.

그러나 새로운 영역은 웹 앱으로부터 생성되고 공존할 수 있으며 모든 새 영역은 위에서 언급한 고유한 집합을 갖게 됩니다.

모든 영역은 에이전트내에 위치하며, 에이전트는 여러 영역들의 부모가 될 수 있습니다. 영역은 자식이나 형제 영역을 가질 수 있습니다.

에이전트에 관해서는 다른 글에서 다룰 예정입니다. 우선 에이전트가 호스트하는 영역에 제공할 수 있는 여러 리소스를 가진 엔티티라는 것을 알아두시길 바랍니다. (ex. 이벤트 루프)

브라우저에서, 영역은 다양한 방식으로 생성될 수 있으며 같은 에이전트의 자식이 될지 여부는 영역의 특성과 서로 간의 관계에 따라 다릅니다. 관련하여 몇 가지 예시가 있습니다.

  1. 같은 출처(origin)의 두 iframe (부모 자식 관계 또는 형제 관계)은 단일 에이전트 내에서 두 개의 영역을 생성합니다.
  2. 다른 출처의 두 iframe (부모 자식 관계 또는 형제 관계)은 서로 다른 에이전트 내에서 두 개의 영역을 생성합니다. (게다가, 교차 출처 사이트 격리를 준수하려면 두 영역의 부모 에이전트는 서로 다른 프로세스에서 동작하는 에이전트 클러스터의 자식이어야 합니다.)
  3. 탑 메인 영역과 서비스 워커는 단일 에이전트 클러스터 내의 서로 다른 에이전트에 위치한 두 개의 영역입니다 (따라서 웹 워커 입니다).

이러한 관계는 또한 영역이 서로 간에 소통할 수 있는 정도를 나타냅니다.

같은 출처의 iframe 영역은 단일 이벤트 루프를 공유하며 contentWindow속성을 통해 서로의 환경을 자유롭고 동기적인 방식으로 접근할 수 있습니다.

// https://example.com
const ifr = document.createElement('iframe');
ifr.src = 'https://example.com'; // 동일 출처
ifr.onload = () => {
    console.log(ifr.contentWindow.document.body);
    // <body></body>
}
document.body.appendChild(ifr);

하지만 교차 출처의 iframe 영역은 같은 API를 이용해서 접근하는 데 훨씬 제한적입니다.

// https://example.com
const ifr = document.createElement('iframe');
ifr.src = '//cross.origin.com'; // 교차 출처
ifr.onload = () => {
    console.log(ifr.contentWindow.document.body);
    // Uncaught DOMException: Blocked a frame with origin "https://example.com" from accessing a cross-origin frame.
}
document.body.appendChild(ifr);

교차 출처 영역들은 그래도 서로 소통할 수 있지만, 의사소통은 훨씬 제한적이며 postMessage()라는 비동기 API를 기반으로 이뤄집니다. 이는 또한 웹 워커, 서비스 워커 등과 소통할 때도 동일하게 적용됩니다.

앞서 설명한 제한에 대한 새롭고 흥미로운 보완 해결책이 곧 그림자 영역 제안에 도입될 예정입니다. 이는 계속 지켜볼만 합니다!

각 영역의 고유성은 영역이 무엇인지 더 잘 이해할 수 있는 좋은 방법입니다.

예를 들어 아래와 같은 웹 사이트를 로드한다고 해봅시다.

<html>
    <head></head>
    <body>
        <iframe id="some_iframe"></iframe>
    </body>
</html>

그러면 탑 메인 영역과 iframe 내의 영역인 두 개의 서로 다른 영역이 있습니다. 그러므로 각 영역은 고유한 전역 객체 및 전역 실행 환경과 함께 고유한 정체성을 갖습니다.

window === some_iframe.contentWindow // false

그리고 각 영역은 각자의 고유 객체와 플랫폼 별 API를 갖습니다.

window.fetch === some_iframe.contentWindow.fetch // false
window.Array === some_iframe.contentWindow.Array // false
<html>
    <script> 
        window.top_array = []; 
    </script>
    <iframe> 
        <script> 
            window.top.iframe_array = []; 
        </script> 
    </iframe>
    <script>
        // top_array 와 iframe_array 은 서로 다른 영영에서 생성되었습니다.
        Object.getPrototypeOf(window.iframe_array) === Object.getPrototypeOf(window.top_array) // false
    </script>
</html>

하지만 원시값은 영역 전체에서 동일합니다.

window.Infinity === some_iframe.contentWindow.Infinity // true

정체성 단절

정체성 단절은 영역이 기능으로 존재하기 때문에 도달할 수 있는 상태이며, 영역이 얼마나 고유한지 강조하는데 도움이 됩니다.

개념을 자세히 알아보기 위해 instanceof 연산자를 사용할 것입니다.

파란 버튼을 생성하고 iframe을 통해 로드하는 타사 서비스가 있다고 상상해 보세요. 웹 앱은 해당 서비스를 아래와 같은 방식으로 사용합니다.

<html>
    <iframe id="blue_buttons_iframe">
        <script>
            window.top.createBlueButton = function(text) {
                const button = document.createElement('button');
                button.style.color = 'blue';
                button.value = text;
                return button;
            };
        </script>
    </iframe>
    <body>
        <script>
            const blueButton = window.createBlueButton('my blue button');
            if (!blueButton instanceof HTMLButtonElement) {
                throw new Error('blue button created does not seem to actually be a button element!');
            }
            document.body.appendChild(blueButton);
        </script>
    </body>
</html>

instanceof를 사용하면 연산자 왼쪽에 있는 것이 연산자 오른쪽의 인스턴스인지 여부를 알 수 있습니다. 예를 들어, button요소는 HTMLButtonElement인터페이스의 인스턴스이기 때문에 document.createElement('button') instanceof HTMLButtonElement의 결과는 true인 반면에, document.createElement('div') instanceof HTMLButtonElementfalse입니다. 왜냐하면 div요소는 명백히 HTMLButtonElement가 아닌 HTMLDivElement에서 상속되기 때문입니다.

그러나, 위의 예제에서 blueButtonHTMLButtonElement에서 상속받지만 instanceof 검사는 false를 반환하고 사용자 지정 오류가 발생합니다.

어떻게 이게 가능할까요? 이는 일반적으로 HTMLButtonElement에서 상속받는 것은 "instance of"로 간주하기에는 부족하므로 발생합니다. 테스트하려는 객체는 반드시 해당 객체가 위치한 특정 영역의 인터페이스에 대한 인스턴스여야 합니다.

원래 instanceof 검사의 의도는 파란 버튼 타사 서비스가 실제로 버튼 요소만 제공하고 다른 것들은 제공하지 않는지 확인하는 것이었습니다. 하지만 실제로 파란 버튼은 HTMLButtonElement 인터페이스 위치한 곳으로부터 다른 영역에서 생성되었으므로 instanceof 검사는 언제나 false를 반환하게 됩니다.

위에서 설명한 버그는 영역과 영역이 제공하는 것이 얼마나 고유한지를 보여주기 위해 코드에 정제성 단절을 도입했기 때문에 발생합니다.

정체성 단절을 해결하는 것이 항상 사소하지는 않습니다. 위의 예제에서 검사를 blueButton instanceof blue_buttons_iframe.contentWindow.HTMLButtonElement로 변경하면 문제가 해결되지만, 이는 확장하기 쉽거나 편리한 해결책은 아닙니다.

객체가 인터페이스의 인스턴스라면, 객체는 반드시 해당 인터페이스의 정확한 영역에서 생성되거나 파생되어야 합니다.

요약

저는 영역이 무엇인지 그리고 무엇이 영역을 정의하는지에 대한 유용하고 정확한, 그리고 쉽게 풀어놓은 정보를 찾을 수 없었기 때문에 이 콘텐츠를 준비했습니다. 일반적으로 공급망 공격 및 보안에서 영역의 역할에 대해 더 깊이 탐구하기 위해 저에게는 영역을 완전히 이해하는 것이 중요했습니다. 이 내용이 도움이 되셨길 바랍니다.

awesome-JavaScript-realms-security 저장소에서 해당 영역에 관한 저의 연구와 개발을 확인하실 수 있습니다.

또한 LavaMoat🌋Snow-JS❄️ 도구를 자세히 알아보고 자바스크립트의 영역을 보호하기 위한 방어적인 보안 노력에 대해 이해해보시길 권장합니다.

1개의 댓글

comment-user-thumbnail
2023년 1월 11일

멋져요 또 좋은 글 써주세요

답글 달기