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

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