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

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

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

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

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

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

ด้วยการแสดงไฟล์ HTML ที่ต้องการภายในแพ็กเกจส่วนขยายว่าเป็นแบบแซนด์บ็อกซ์ เมื่อใดก็ตามที่โหลดหน้าเว็บที่มีแซนด์บ็อกซ์ ระบบจะย้ายหน้าดังกล่าวไปยังต้นทางที่ไม่ซ้ำกันและจะเข้าถึง API ของ chrome.* ไม่ได้ หากเราโหลดหน้าแซนด์บ็อกซ์นี้ไปยังส่วนขยายของเราผ่านทาง 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 เครื่องมือจะโหลดไลบรารีของ Handlebar รวมถึงสร้างและคอมไพล์เทมเพลตอินไลน์ในลักษณะที่แฮนเดิลแนะนำ

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