샌드박스 처리된 iframe에서 eval() 사용

Chrome의 확장 프로그램 시스템은 다소 엄격한 기본 콘텐츠 보안 정책 (CSP)을 적용합니다. 정책 제한사항은 간단합니다. 스크립트는 인라인에서 별도의 JavaScript 파일로 이동해야 하고, 인라인 이벤트 핸들러는 addEventListener를 사용하도록 변환해야 하며, eval()은 사용 중지됩니다.

하지만 다양한 라이브러리에서 성능 최적화 및 표현의 용이성을 위해 eval()eval과 유사한 구성(예: new Function())을 사용한다는 점을 알고 있습니다. 템플릿 라이브러리는 특히 이러한 구현 스타일에 취약합니다. 일부 (예: Angular.js)는 기본적으로 CSP를 지원하지만, 많은 인기 프레임워크는 아직 확장 프로그램의 eval이 없는 세계와 호환되는 메커니즘으로 업데이트되지 않았습니다. 따라서 이 기능에 대한 지원을 삭제하는 것은 개발자에게 예상보다 더 큰 문제 를 일으켰습니다.

이 문서에서는 보안을 손상시키지 않고 프로젝트에 이러한 라이브러리를 포함하는 안전한 메커니즘으로 샌드박스를 소개합니다.

샌드박스를 사용하는 이유

eval 은 확장 프로그램 내에서 위험합니다. 실행하는 코드가 확장 프로그램의 높은 권한 환경에 있는 모든 항목에 액세스할 수 있기 때문입니다. 사용자의 보안과 개인 정보 보호에 심각한 영향을 미칠 수 있는 강력한 chrome.* API가 많이 있습니다. 간단한 데이터 무단 반출은 가장 작은 문제입니다. 제공되는 솔루션은 eval이 확장 프로그램의 데이터 또는 확장 프로그램의 가치 높은 API에 액세스하지 않고 코드를 실행할 수 있는 샌드박스입니다. 데이터도 없고 API도 없으니 문제도 없습니다.

확장 프로그램 패키지 내의 특정 HTML 파일을 샌드박스 처리된 것으로 나열하여 이 작업을 수행합니다. 샌드박스 처리된 페이지가 로드될 때마다 고유한 출처로 이동되고 chrome.* API에 대한 액세스가 거부됩니다. iframe을 통해 이 샌드박스 처리된 페이지를 확장 프로그램에 로드하면 메시지를 전달하고, 메시지에 따라 어떤 방식으로든 작동하도록 하고, 결과를 다시 전달해 줄 때까지 기다릴 수 있습니다. 이 간단한 메시지 메커니즘은 확장 프로그램의 워크플로에 eval 기반 코드를 안전하게 포함하는 데 필요한 모든 것을 제공합니다.

샌드박스 만들기 및 사용

코드로 바로 이동하려면 샌드박스 처리 샘플 확장 프로그램을 가져와서 시작하세요. Handlebars 템플릿 라이브러리를 기반으로 빌드된 작은 메시지 API의 작동 예시이며, 시작하는 데 필요한 모든 것을 제공합니다. 좀 더 자세한 설명을 원하는 사용자를 위해 여기에서 샘플을 함께 살펴보겠습니다.

매니페스트의 파일 나열

샌드박스 내에서 실행해야 하는 각 파일은 sandbox 속성을 추가하여 확장 프로그램 매니페스트에 나열해야 합니다. 이 단계는 매우 중요하며 잊기 쉬우므로 샌드박스 처리된 파일이 매니페스트에 나열되어 있는지 다시 한번 확인하세요. 이 샘플에서는 'sandbox.html'이라는 이름의 파일을 샌드박스 처리합니다. 매니페스트 항목은 다음과 같습니다.

{
  ...,
  "sandbox": {
     "pages": ["sandbox.html"]
  },
  ...
}

샌드박스 처리된 파일 로드

샌드박스 처리된 파일로 흥미로운 작업을 하려면 확장 프로그램의 코드로 주소를 지정할 수 있는 컨텍스트에서 파일을 로드해야 합니다. 여기에서는 iframe을 사용하여 sandbox.html이 확장 프로그램 페이지에 로드되었습니다. 페이지의 JavaScript 파일에는 페이지에서 iframe을 찾고 contentWindow에서 postMessage()를 호출하여 브라우저 작업이 클릭될 때마다 샌드박스에 메시지를 보내는 코드가 포함되어 있습니다. 메시지는 context, templateName, command라는 세 가지 속성이 포함된 객체입니다. 잠시 후 contextcommand를 자세히 살펴보겠습니다.

service-worker.js:

chrome.action.onClicked.addListener(() => {
  chrome.tabs.create({
    url: 'mainpage.html'
  });
  console.log('Opened a tab with a sandboxed page!');
});

extension-page.js:

let counter = 0;
document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('reset').addEventListener('click', function () {
    counter = 0;
    document.querySelector('#result').innerHTML = '';
  });

  document.getElementById('sendMessage').addEventListener('click', function () {
    counter++;
    let message = {
      command: 'render',
      templateName: 'sample-template-' + counter,
      context: { counter: counter }
    };
    document.getElementById('theFrame').contentWindow.postMessage(message, '*');
  });

위험한 작업 실행

sandbox.html이 로드되면 Handlebars 라이브러리를 로드하고 Handlebars에서 제안하는 방식으로 인라인 템플릿을 만들고 컴파일합니다.

extension-page.html:

<!DOCTYPE html>
<html>
  <head>
    <script src="mainpage.js"></script>
    <link href="styles/main.css" rel="stylesheet" />
  </head>
  <body>
    <div id="buttons">
      <button id="sendMessage">Click me</button>
      <button id="reset">Reset counter</button>
    </div>

    <div id="result"></div>

    <iframe id="theFrame" src="sandbox.html" style="display: none"></iframe>
  </body>
</html>

sandbox.html:

   <script id="sample-template-1" type="text/x-handlebars-template">
      <div class='entry'>
        <h1>Hello</h1>
        <p>This is a Handlebar template compiled inside a hidden sandboxed
          iframe.</p>
        <p>The counter parameter from postMessage() (outer frame) is:
          </p>
      </div>
    </script>

    <script id="sample-template-2" type="text/x-handlebars-template">
      <div class='entry'>
        <h1>Welcome back</h1>
        <p>This is another Handlebar template compiled inside a hidden sandboxed
          iframe.</p>
        <p>The counter parameter from postMessage() (outer frame) is:
          </p>
      </div>
    </script>

실패하지 않습니다. Handlebars.compile이 결국 new Function을 사용하더라도 모든 것이 예상대로 작동하며 templates['hello']에 컴파일된 템플릿이 생성됩니다.

결과 다시 전달

확장 프로그램 페이지의 명령어를 수락하는 메시지 리스너를 설정하여 이 템플릿을 사용할 수 있도록 하겠습니다. 전달된 command를 사용하여 수행해야 할 작업을 결정합니다 (렌더링 외에 템플릿을 만드는 등의 작업을 할 수 있다고 상상해 보세요. 어떤 방식으로든 관리할 수 있겠죠?). context는 렌더링을 위해 템플릿에 직접 전달됩니다. 렌더링된 HTML은 확장 프로그램이 나중에 유용하게 사용할 수 있도록 확장 프로그램 페이지로 다시 전달됩니다.

 <script>
      const templatesElements = document.querySelectorAll(
        "script[type='text/x-handlebars-template']"
      );
      let templates = {},
        source,
        name;

      // precompile all templates in this page
      for (let i = 0; i < templatesElements.length; i++) {
        source = templatesElements[i].innerHTML;
        name = templatesElements[i].id;
        templates[name] = Handlebars.compile(source);
      }

      // Set up message event handler:
      window.addEventListener('message', function (event) {
        const command = event.data.command;
        const template = templates[event.data.templateName];
        let result = 'invalid request';

       // if we don't know the templateName requested, return an error message
        if (template) {
          switch (command) {
            case 'render':
              result = template(event.data.context);
              break;
            // you could even do dynamic compilation, by accepting a command
            // to compile a new template instead of using static ones, for example:
            // case 'new':
            //   template = Handlebars.compile(event.data.templateSource);
            //   result = template(event.data.context);
            //   break;
              }
        } else {
            result = 'Unknown template: ' + event.data.templateName;
        }
        event.source.postMessage({ result: result }, event.origin);
      });
    </script>

확장 프로그램 페이지로 돌아가서 이 메시지를 수신하고 전달된 html 데이터로 흥미로운 작업을 실행합니다. 이 경우 알림을 통해 에코하지만 이 HTML을 확장 프로그램의 UI의 일부로 안전하게 사용할 수 있습니다. innerHTML을 통해 삽입해도 샌드박스 내에서 렌더링된 콘텐츠를 신뢰하므로 심각한 보안 위험이 발생하지 않습니다.

이 메커니즘을 사용하면 템플릿을 간단하게 만들 수 있지만 템플릿에만 국한되지는 않습니다. 엄격한 콘텐츠 보안 정책에서 기본적으로 작동하지 않는 코드는 샌드박스 처리할 수 있습니다. 실제로 각 프로그램이 올바르게 실행하는 데 필요한 최소한의 권한으로 제한하기 위해 올바르게 실행되는 확장 프로그램의 구성요소를 샌드박스 처리하는 것이 유용한 경우가 많습니다. Google I/O 2012의 안전한 웹 앱 및 Chrome 확장 프로그램 작성 프레젠테이션에서는 이러한 기법이 실제로 사용되는 좋은 예를 제공하며 56분의 시간을 투자할 가치가 있습니다.