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

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

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

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

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

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

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

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

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

แสดงรายการไฟล์ในไฟล์ 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 แอปจะโหลดไลบรารีของแถบควบคุม รวมทั้งสร้างและคอมไพล์แบบอินไลน์ ตามวิธีที่แฮนด์บาร์แนะนำ:

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 นาที