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

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

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

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

ทำไมต้องใช้แซนด์บ็อกซ์

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

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

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

หากต้องการเจาะลึกเรื่องโค้ดโดยตรง ให้ดาวน์โหลดส่วนขยายตัวอย่างแซนด์บ็อกซ์และเริ่มได้เลย ส่วนขยายนี้เป็นตัวอย่างการทำงานของ 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:

browser.action.onClicked.addListener(() => {
  browser.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 ไปยังเทมเพลตโดยตรงเพื่อทำการแสดงผล ระบบจะส่ง 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 นาที