ใช้ eval() ใน iframe ที่ทำแซนด์บ็อกซ์

ระบบส่วนขยายของ Chrome จะบังคับใช้นโยบายรักษาความปลอดภัยเนื้อหา (CSP) เริ่มต้นที่ค่อนข้างเข้มงวด ข้อจํากัดของนโยบายนั้นเข้าใจง่าย นั่นคือต้องย้ายสคริปต์ออกจากบรรทัดไปยังไฟล์ JavaScript แยกต่างหาก ต้องแปลงตัวแฮนเดิลเหตุการณ์ในบรรทัดให้ใช้ addEventListener และปิดใช้ eval()

อย่างไรก็ตาม เราทราบดีว่าไลบรารีต่างๆ ใช้คอนสตรัคต์ที่คล้ายกับ eval() และ eval เช่น new Function() เพื่อเพิ่มประสิทธิภาพและเพิ่มความสะดวกในการเขียน ไลบรารีเทมเพลตมีแนวโน้มที่จะใช้รูปแบบนี้เป็นพิเศษ แม้ว่าบางเฟรมเวิร์ก (เช่น Angular.js) จะรองรับ CSP อยู่แล้ว แต่เฟรมเวิร์กยอดนิยมหลายรายการยังไม่ได้อัปเดตเป็นกลไกที่เข้ากันได้กับโลกที่ไม่มี eval ของส่วนขยาย ดังนั้นการนำการรองรับฟังก์ชันการทำงานดังกล่าวออกจึงก่อให้เกิดปัญหามากกว่าที่คาดไว้สำหรับนักพัฒนาแอป

เอกสารนี้จะแนะนำแซนด์บ็อกซ์เป็นกลไกที่ปลอดภัยในการรวมไลบรารีเหล่านี้ไว้ในโปรเจ็กต์โดยไม่ลดทอนความปลอดภัย

เหตุผลที่ควรใช้แซนด์บ็อกซ์

eval เป็นอันตรายภายในส่วนขยายเนื่องจากโค้ดที่ดำเนินการมีสิทธิ์เข้าถึงทุกอย่างในสภาพแวดล้อมที่มีสิทธิ์ระดับสูงของส่วนขยาย มี chrome.* API ที่มีประสิทธิภาพมากมายที่อาจส่งผลเสียอย่างมากต่อความปลอดภัยและความเป็นส่วนตัวของผู้ใช้ การนำข้อมูลออกอย่างง่ายดายเป็นเพียงปัญหาเล็กน้อยที่เรากังวล โซลูชันที่เสนอคือแซนด์บ็อกซ์ที่ eval สามารถเรียกใช้โค้ดได้โดยไม่ต้องเข้าถึงข้อมูลของส่วนขยายหรือ API ที่มีมูลค่าสูงของส่วนขยาย ไม่มีข้อมูล ไม่มี API ก็ไม่เป็นไร

เราทําเช่นนี้ได้โดยระบุไฟล์ HTML ที่เฉพาะเจาะจงภายในแพ็กเกจส่วนขยายว่าเป็นไฟล์ที่อยู่ในแซนด์บ็อกซ์ เมื่อใดก็ตามที่โหลดหน้าเว็บที่อยู่ในแซนด์บ็อกซ์ ระบบจะย้ายหน้าเว็บนั้นไปยังต้นทางที่ไม่ซ้ำกัน และปฏิเสธการเข้าถึง chrome.* API หากโหลดหน้าที่อยู่ในแซนด์บ็อกซ์นี้ลงในส่วนขยายผ่าน iframe เราจะส่งข้อความไปให้ ปล่อยให้ดำเนินการกับข้อความเหล่านั้นในลักษณะใดลักษณะหนึ่ง และรอให้ส่งผลลัพธ์กลับมาให้เรา กลไกการรับส่งข้อความที่เรียบง่ายนี้ช่วยให้เรามีทุกสิ่งที่จําเป็นเพื่อรวมโค้ดที่ขับเคลื่อนโดย eval ไว้ในเวิร์กโฟลว์ของส่วนขยายอย่างปลอดภัย

สร้างและใช้แซนด์บ็อกซ์

หากต้องการเริ่มเขียนโค้ดเลย ให้ใช้ส่วนขยายตัวอย่างแซนด์บ็อกซ์และเริ่มต้นใช้งาน ซึ่งเป็นตัวอย่างที่ใช้งานได้จริงของ Tiny Messaging API ที่สร้างขึ้นบนไลบรารีเทมเพลต Handlebars และควรมีทุกอย่างที่คุณต้องการเพื่อเริ่มต้นใช้งาน สําหรับผู้ที่ต้องการคําอธิบายเพิ่มเติม เรามาลองดูตัวอย่างนี้กัน

แสดงรายการไฟล์ในไฟล์ Manifest

ไฟล์แต่ละไฟล์ที่ควรเรียกใช้ภายในแซนด์บ็อกซ์ต้องแสดงอยู่ในไฟล์ Manifest ของส่วนขยายด้วยการเพิ่มพร็อพเพอร์ตี้ sandbox ขั้นตอนนี้เป็นขั้นตอนสำคัญและคุณอาจลืมได้ง่ายๆ ดังนั้นโปรดตรวจสอบอีกครั้งว่าไฟล์ที่อยู่ในแซนด์บ็อกซ์แสดงอยู่ในไฟล์ Manifest ในตัวอย่างนี้ เรากำลังใช้แซนด์บ็อกซ์กับไฟล์ที่ชื่อ "sandbox.html" รายการไฟล์ Manifest จะมีลักษณะดังนี้

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

โหลดไฟล์ที่อยู่ในแซนด์บ็อกซ์

หากต้องการทําสิ่งที่น่าสนใจกับไฟล์ในแซนด์บ็อกซ์ เราจําเป็นต้องโหลดไฟล์ในบริบทที่โค้ดของส่วนขยายสามารถเข้าถึงได้ ที่นี่ ระบบได้โหลด sandbox.html ลงในหน้าส่วนขยายผ่าน iframe ไฟล์ JavaScript ของหน้าเว็บมีโค้ดที่ส่งข้อความไปยังแซนด์บ็อกซ์ทุกครั้งที่มีการคลิกการดําเนินการของเบราว์เซอร์โดยค้นหา iframe ในหน้าเว็บ และเรียกใช้ postMessage() ใน contentWindow ข้อความคือออบเจ็กต์ที่มีพร็อพเพอร์ตี้ 3 รายการ ได้แก่ context, templateName และ command เราจะพูดถึง context และ command ในอีกสักครู่

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

ส่งผลลัพธ์กลับ

เราจะทําให้เทมเพลตนี้พร้อมใช้งานโดยตั้งค่า Listener ข้อความที่ยอมรับคําสั่งจากหน้าส่วนขยาย เราจะใช้ command ที่ส่งผ่านมาเพื่อพิจารณาสิ่งที่ควรทำ (คุณอาจจินตนาการได้ว่าจะทำมากกว่าแค่การแสดงผล เช่น สร้างเทมเพลต โปรดจัดการ context ดังกล่าวในลักษณะใดลักษณะหนึ่ง) และระบบจะส่ง context ไปยังเทมเพลตโดยตรงเพื่อแสดงผล ระบบจะส่ง HTML ที่แสดงผลกลับไปยังหน้าส่วนขยายเพื่อให้ส่วนขยายทําสิ่งมีประโยชน์กับ 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 ไม่ได้ก่อให้เกิดความเสี่ยงด้านความปลอดภัยที่สำคัญ เนื่องจากเราเชื่อถือเนื้อหาที่แสดงผลภายในแซนด์บ็อกซ์

กลไกนี้ทําให้การสร้างเทมเพลตเป็นเรื่องง่าย แต่แน่นอนว่าไม่ได้จํากัดไว้เพียงการสร้างเทมเพลตเท่านั้น โค้ดใดก็ตามที่ใช้งานไม่ได้ทันทีภายใต้นโยบายความปลอดภัยของเนื้อหาที่เข้มงวดจะใช้แซนด์บ็อกซ์ได้ อันที่จริงแล้ว การใช้แซนด์บ็อกซ์กับคอมโพเนนต์ของส่วนขยายที่ควรจะทํางานได้อย่างถูกต้องเพื่อจํากัดแต่ละส่วนของโปรแกรมให้มีชุดสิทธิ์ที่จําเป็นน้อยที่สุดเพื่อให้ทํางานได้อย่างถูกต้องนั้นมักมีประโยชน์ คุณสามารถดูตัวอย่างที่ดีของเทคนิคเหล่านี้ได้จากงานนำเสนอการเขียนเว็บแอปและส่วนขยาย Chrome ที่ปลอดภัยจาก Google I/O 2012 ซึ่งใช้เวลาเพียง 56 นาที