Sử dụng eval() trong iframe hộp cát

Hệ thống tiện ích của Chrome thực thi một Chính sách bảo mật nội dung (CSP) mặc định khá nghiêm ngặt. Các hạn chế theo chính sách rất đơn giản: tập lệnh phải được di chuyển ra khỏi dòng vào các tệp JavaScript riêng biệt, trình xử lý sự kiện cùng dòng phải được chuyển đổi để sử dụng addEventListenereval() bị vô hiệu hoá.

Tuy nhiên, chúng tôi nhận thấy nhiều thư viện sử dụng các cấu trúc tương tự như eval()eval, chẳng hạn như new Function() để tối ưu hoá hiệu suất và dễ dàng thể hiện. Các thư viện tạo mẫu đặc biệt dễ bị ảnh hưởng bởi kiểu triển khai này. Mặc dù một số (như Angular.js) hỗ trợ CSP ngay từ đầu, nhưng nhiều khung hình phổ biến vẫn chưa cập nhật lên cơ chế tương thích với thế giới không có eval của các tiện ích. Do đó, việc loại bỏ tính năng này đã chứng minh là gây ra nhiều vấn đề hơn dự kiến cho nhà phát triển.

Tài liệu này giới thiệu tính năng hộp cát như một cơ chế an toàn để đưa các thư viện này vào dự án của bạn mà không ảnh hưởng đến tính bảo mật.

Tại sao nên dùng hộp cát?

eval là một quyền nguy hiểm trong tiện ích vì mã mà quyền này thực thi có quyền truy cập vào mọi thứ trong môi trường có quyền cao của tiện ích. Có rất nhiều API chrome.* mạnh mẽ có thể ảnh hưởng nghiêm trọng đến tính bảo mật và quyền riêng tư của người dùng; việc đánh cắp dữ liệu đơn giản là điều ít lo ngại nhất. Giải pháp được cung cấp là một hộp cát mà eval có thể thực thi mã mà không cần truy cập vào dữ liệu của tiện ích hoặc các API có giá trị cao của tiện ích. Không có dữ liệu, không có API cũng chẳng sao cả.

Chúng tôi thực hiện việc này bằng cách liệt kê các tệp HTML cụ thể bên trong gói tiện ích là được cách ly. Bất cứ khi nào một trang được cách ly được tải, trang đó sẽ được chuyển đến một nguồn gốc duy nhất và sẽ bị từ chối quyền truy cập vào các API chrome.*. Nếu tải trang được cách ly này vào tiện ích thông qua iframe, chúng ta có thể truyền thông báo cho trang đó, cho phép trang đó thực hiện hành động đối với những thông báo đó theo một cách nào đó và đợi trang đó truyền lại cho chúng ta một kết quả. Cơ chế nhắn tin đơn giản này cung cấp cho chúng ta mọi thứ cần thiết để đưa mã do eval điều khiển vào quy trình công việc của tiện ích một cách an toàn.

Tạo và sử dụng hộp cát

Nếu bạn muốn đi thẳng vào mã, hãy lấy tiện ích mẫu tạo hộp cát và bắt đầu. Đây là một ví dụ hoạt động về API nhắn tin nhỏ được xây dựng trên thư viện tạo mẫu Handlebars và sẽ cung cấp cho bạn mọi thứ bạn cần để bắt đầu. Nếu bạn muốn tìm hiểu thêm, hãy cùng xem qua ví dụ đó tại đây.

Liệt kê các tệp trong tệp kê khai

Mỗi tệp cần chạy trong hộp cát phải được liệt kê trong tệp kê khai tiện ích bằng cách thêm thuộc tính sandbox. Đây là một bước quan trọng và rất dễ quên, vì vậy, hãy kiểm tra kỹ để đảm bảo tệp được cách ly của bạn có trong tệp kê khai. Trong mẫu này, chúng ta đang tạo hộp cát cho tệp có tên "sandbox.html". Mục nhập trong tệp kê khai có dạng như sau:

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

Tải tệp hộp cát

Để làm điều gì đó thú vị với tệp được cách ly, chúng ta cần tải tệp đó trong một ngữ cảnh mà mã của tiện ích có thể giải quyết được. Ở đây, sandbox.html đã được tải vào một trang tiện ích bằng cách sử dụng iframe. Tệp JavaScript của trang chứa mã gửi thông báo vào hộp cát bất cứ khi nào người dùng nhấp vào thao tác của trình duyệt bằng cách tìm iframe trên trang và gọi postMessage() trên contentWindow của trang. Thông báo này là một đối tượng chứa 3 thuộc tính: context, templateNamecommand. Chúng ta sẽ tìm hiểu về contextcommand ngay sau đây.

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, '*');
  });

Làm điều gì đó nguy hiểm

Khi sandbox.html được tải, nó sẽ tải thư viện Handlebars, đồng thời tạo và biên dịch một mẫu cùng dòng theo cách mà Handlebars đề xuất:

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>

Thao tác này không thất bại! Mặc dù Handlebars.compile cuối cùng sử dụng new Function, mọi thứ vẫn hoạt động đúng như dự kiến và chúng ta sẽ có một mẫu đã biên dịch trong templates['hello'].

Truyền kết quả trở lại

Chúng ta sẽ cung cấp mẫu này để sử dụng bằng cách thiết lập một trình nghe tin nhắn chấp nhận các lệnh từ trang tiện ích. Chúng ta sẽ sử dụng command được truyền vào để xác định những việc cần làm (bạn có thể tưởng tượng việc làm nhiều hơn là chỉ kết xuất; có lẽ là tạo các mẫu? Có lẽ là quản lý chúng theo cách nào đó?), và context sẽ được truyền trực tiếp vào mẫu để hiển thị. HTML đã kết xuất sẽ được chuyển lại cho trang tiện ích để tiện ích có thể làm điều gì đó hữu ích với HTML đó sau này:

 <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>

Quay lại trang tiện ích, chúng ta sẽ nhận được thông báo này và làm điều gì đó thú vị với dữ liệu html mà chúng ta đã truyền. Trong trường hợp này, chúng ta chỉ cần truyền thông tin đó qua một thông báo, nhưng hoàn toàn có thể sử dụng HTML này một cách an toàn trong giao diện người dùng của tiện ích. Việc chèn mã này thông qua innerHTML không gây ra rủi ro bảo mật đáng kể vì chúng tôi tin tưởng nội dung đã được hiển thị trong hộp cát.

Cơ chế này giúp việc tạo mẫu trở nên đơn giản, nhưng tất nhiên là nó không chỉ giới hạn ở việc tạo mẫu. Mọi mã không hoạt động ngay trong Chính sách bảo mật nội dung nghiêm ngặt đều có thể được đưa vào hộp cát; trên thực tế, việc đưa các thành phần của tiện ích mà sẽ chạy đúng cách vào hộp cát thường rất hữu ích để hạn chế mỗi phần của chương trình ở mức đặc quyền nhỏ nhất cần thiết để chương trình thực thi đúng cách. Bài thuyết trình Viết các ứng dụng web và tiện ích Chrome an toàn của Google I/O 2012 đưa ra một số ví dụ hay về cách thức hoạt động của kỹ thuật này và đáng để bạn dành 56 phút để xem.