การใช้ eval ในส่วนขยาย Chrome

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

อย่างไรก็ตาม เราตระหนักดีว่าไลบรารีต่างๆ ใช้โครงสร้างที่คล้ายกับ 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 โหลดลงในหน้าเหตุการณ์ (eventpage.html) ของส่วนขยายผ่าน iframe eventpage.js มีโค้ด ซึ่งจะส่งข้อความไปยังแซนด์บ็อกซ์เมื่อใดก็ตามที่มีการคลิกการทำงานของเบราว์เซอร์ โดยการค้นหา iframe ในหน้า และเรียกใช้เมธอด postMessage ใน contentWindow ข้อความคือออบเจ็กต์ที่มีพร็อพเพอร์ตี้ 2 รายการ ได้แก่ context และ command เราจะเจาะลึกเกี่ยวกับทั้งสองนี้ในอีกสักครู่

chrome.browserAction.onClicked.addListener(function() {
 var iframe = document.getElementById('theFrame');
 var message = {
   command: 'render',
   context: {thing: 'world'}
 };
 iframe.contentWindow.postMessage(message, '*');
});
ดูข้อมูลทั่วไปเกี่ยวกับ postMessage API ได้ที่เอกสารประกอบ postMessage เกี่ยวกับ MDN เนื้อหาค่อนข้างสมบูรณ์และควรค่าแก่การอ่าน โดยเฉพาะอย่างยิ่ง โปรดทราบว่าข้อมูลจะส่งต่อกลับมาได้ต่อเมื่อเรียงลำดับได้เท่านั้น ตัวอย่างเช่น ฟังก์ชันไม่เป็นเช่นนั้น

กระทำการที่เป็นอันตราย

เมื่อโหลด sandbox.html ระบบจะโหลดไลบรารี Handlebars และสร้างและคอมไพล์เทมเพลตแบบอินไลน์ในลักษณะที่ Handlebars แนะนำ ดังนี้

<script src="handlebars-1.0.0.beta.6.js"></script>
<script id="hello-world-template" type="text/x-handlebars-template">
  <div class="entry">
    <h1>Hello, !</h1>
  </div>
</script>
<script>
  var templates = [];
  var source = document.getElementById('hello-world-template').innerHTML;
  templates['hello'] = Handlebars.compile(source);
</script>

นี่ไม่ล้มเหลว แม้ว่า Handlebars.compile จะใช้ new Function แล้ว แต่ก็ยังมีสิ่งต่างๆ อยู่ ตามที่คาดไว้ และในที่สุดเราก็ได้เทมเพลตที่รวบรวมไว้ใน templates['hello']

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

เราจะทำให้เทมเพลตนี้พร้อมใช้งานโดยการตั้งค่า Listener ข้อความที่ยอมรับคำสั่ง จากหน้ากิจกรรม เราจะใช้ command ที่ส่งผ่านมาเพื่อพิจารณาสิ่งที่ควรทำ (คุณอาจจินตนาการได้ว่าจะทำมากกว่าแค่การแสดงผล เช่น สร้างเทมเพลต หรือจะจัดการด้วยวิธีใดก็ได้) และระบบจะส่ง context ไปยังเทมเพลตโดยตรงเพื่อแสดงผล HTML ที่แสดงผล จะถูกส่งกลับไปยังหน้ากิจกรรมเพื่อให้ส่วนขยายสามารถทำสิ่งที่มีประโยชน์กับคุณในภายหลัง

<script>
  window.addEventListener('message', function(event) {
    var command = event.data.command;
    var name = event.data.name || 'hello';
    switch(command) {
      case 'render':
        event.source.postMessage({
          name: name,
          html: templates[name](event.data.context)
        }, event.origin);
        break;

      // case 'somethingElse':
      //   ...
    }
  });
</script>

กลับไปที่หน้ากิจกรรม เราได้รับข้อความนี้ พร้อมกับทำสิ่งที่น่าสนใจกับ html ซึ่งเราส่งผ่านไป ในกรณีนี้ เราจะแสดงผลผ่านการแจ้งเตือนบนเดสก์ท็อปเท่านั้น แต่คุณใช้ HTML นี้ได้อย่างปลอดภัยเป็นส่วนหนึ่งของ UI ของส่วนขยาย การแทรกผ่าน innerHTML ไม่ได้ก่อให้เกิดความเสี่ยงด้านความปลอดภัยที่สำคัญ เนื่องจากแม้จะมีการบุกรุกโค้ดในแซนด์บ็อกซ์อย่างสมบูรณ์ผ่านการโจมตีที่ชาญฉลาด แต่ก็ไม่สามารถแทรกสคริปต์หรือเนื้อหาปลั๊กอินที่เป็นอันตรายลงในบริบทส่วนขยายที่มีสิทธิ์ระดับสูงได้

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