ระบบส่วนขยายของ Chrome บังคับใช้นโยบายรักษาความปลอดภัยเนื้อหา (CSP) เริ่มต้นที่ค่อนข้างเข้มงวด
ข้อจำกัดของนโยบายนั้นตรงไปตรงมา โดยต้องย้ายสคริปต์ออกจากบรรทัดไปไว้ในไฟล์ JavaScript แยกต่างหาก ต้องแปลงตัวแฮนเดิลเหตุการณ์แบบอินไลน์ให้ใช้ addEventListener และปิดใช้ eval()
อย่างไรก็ตาม เราทราบดีว่าไลบรารีต่างๆ ใช้ eval() และโครงสร้างที่คล้ายกับ
eval เช่นnew Function() เพื่อเพิ่มประสิทธิภาพและแสดงออกได้ง่าย ไลบรารีการสร้างเทมเพลตมีแนวโน้มที่จะใช้การติดตั้งใช้งานสไตล์นี้เป็นพิเศษ แม้ว่าบางไลบรารี (เช่น Angular.js) จะรองรับ CSP ได้ทันที
แต่เฟรมเวิร์กยอดนิยมหลายรายการยังไม่ได้อัปเดตกลไกให้เข้ากันได้กับ
โลกที่ไม่มี eval ของส่วนขยาย ดังนั้น การนำการรองรับฟังก์ชันการทำงานดังกล่าวออกจึงก่อให้เกิดปัญหามากกว่า
ที่นักพัฒนาซอฟต์แวร์คาดไว้
เอกสารนี้จะแนะนำการใช้แซนด์บ็อกซ์เป็นกลไกที่ปลอดภัยในการรวมไลบรารีเหล่านี้ไว้ในโปรเจ็กต์โดยไม่กระทบต่อความปลอดภัย
เหตุใดจึงต้องใช้แซนด์บ็อกซ์
eval เป็นอันตรายภายในส่วนขยายเนื่องจากโค้ดที่เรียกใช้มีสิทธิ์เข้าถึงทุกอย่างในสภาพแวดล้อมที่มีสิทธิ์สูงของส่วนขยาย มี chrome.* API ที่มีประสิทธิภาพมากมายที่อาจส่งผลกระทบอย่างรุนแรงต่อความปลอดภัยและความเป็นส่วนตัวของผู้ใช้ การขโมยข้อมูลอย่างง่ายเป็นเรื่องที่เรากังวลน้อยที่สุด
โซลูชันที่เรานำเสนอคือแซนด์บ็อกซ์ที่ eval สามารถเรียกใช้โค้ดได้โดยไม่ต้องเข้าถึงข้อมูลของส่วนขยายหรือ API ที่มีมูลค่าสูงของส่วนขยาย ไม่มีข้อมูล ไม่มี API ก็ไม่มีปัญหา
เราทำได้โดยการระบุไฟล์ HTML ที่เฉพาะเจาะจงภายในแพ็กเกจส่วนขยายว่าเป็นไฟล์ที่อยู่ในแซนด์บ็อกซ์
เมื่อใดก็ตามที่โหลดหน้าเว็บที่อยู่ในแซนด์บ็อกซ์ ระบบจะย้ายหน้าเว็บนั้นไปยังต้นทางที่ไม่ซ้ำกัน และจะปฏิเสธ
การเข้าถึง chrome.* API หากเราโหลดหน้าเว็บที่อยู่ในแซนด์บ็อกซ์นี้ลงในส่วนขยายผ่าน iframe เราจะส่งข้อความไปยังหน้าเว็บนั้นได้ ให้หน้าเว็บดำเนินการกับข้อความเหล่านั้นในบางวิธี และรอให้หน้าเว็บส่งผลลัพธ์กลับมาให้เรา กลไกการรับส่งข้อความอย่างง่ายนี้ช่วยให้เรามีทุกสิ่งที่จำเป็นในการรวมโค้ดที่ขับเคลื่อนด้วย eval ไว้ในเวิร์กโฟลว์ของส่วนขยายได้อย่างปลอดภัย
สร้างและใช้แซนด์บ็อกซ์
หากต้องการดูโค้ดโดยตรง ให้ดาวน์โหลดส่วนขยายตัวอย่างการใช้แซนด์บ็อกซ์แล้วเริ่มใช้งานได้เลย ส่วนขยายนี้เป็นตัวอย่างการทำงานของ API การรับส่งข้อความขนาดเล็กที่สร้างขึ้นบนไลบรารีการสร้างเทมเพลต Handlebars และควรมีทุกสิ่งที่คุณต้องการเพื่อเริ่มต้นใช้งาน สำหรับผู้ที่ต้องการคำอธิบายเพิ่มเติมเล็กน้อย เราจะดูตัวอย่างนั้นด้วยกันที่นี่
ระบุไฟล์ในไฟล์ Manifest
ไฟล์แต่ละไฟล์ที่ควรทำงานภายในแซนด์บ็อกซ์ต้องระบุไว้ในไฟล์ Manifest ของส่วนขยายโดยการเพิ่มพร็อพเพอร์ตี้ sandbox นี่เป็นขั้นตอนที่สำคัญและลืมได้ง่าย ดังนั้นโปรดตรวจสอบอีกครั้งว่าไฟล์ที่อยู่ในแซนด์บ็อกซ์ระบุไว้ในไฟล์ Manifest ในตัวอย่างนี้ เราจะใช้แซนด์บ็อกซ์กับไฟล์ที่ชื่อ "sandbox.html" อย่างชาญฉลาด รายการไฟล์ Manifest จะมีลักษณะดังนี้
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
โหลดไฟล์ที่อยู่ในแซนด์บ็อกซ์
หากต้องการทำสิ่งต่างๆ ที่น่าสนใจกับไฟล์ที่อยู่ในแซนด์บ็อกซ์ เราต้องโหลดไฟล์นั้นในบริบทที่โค้ดของส่วนขยายสามารถเข้าถึงได้ ในที่นี้ sandbox.html ได้รับการโหลดลงในหน้าส่วนขยายโดยใช้ iframe ไฟล์ JavaScript ของหน้าเว็บมีโค้ดที่ส่งข้อความไปยังแซนด์บ็อกซ์ทุกครั้งที่ผู้ใช้คลิกการดำเนินการของเบราว์เซอร์ โดยการค้นหา iframe ในหน้าเว็บและเรียกใช้ postMessage() ใน contentWindow ของ iframe ข้อความจะเป็นออบเจ็กต์ที่มีพร็อพเพอร์ตี้ 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 ระบบจะโหลดไลบรารี 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']
ส่งผลลัพธ์กลับ
เราจะทำให้เทมเพลตนี้พร้อมใช้งานโดยการตั้งค่าตัวฟังข้อความที่ยอมรับคำสั่งจากหน้าส่วนขยาย เราจะใช้ command ที่ส่งมาเพื่อกำหนดสิ่งที่ควรทำ (คุณอาจทำมากกว่าการแสดงผลเพียงอย่างเดียว เช่น การสร้างเทมเพลต หรือการจัดการเทมเพลตในบางวิธี) และระบบจะส่ง context ไปยังเทมเพลตโดยตรงเพื่อแสดงผล ระบบจะส่ง HTML ที่แสดงผลแล้วกลับไปยังหน้าส่วนขยายเพื่อให้ส่วนขยายสามารถนำ 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 ของส่วนขยายได้ การแทรก HTML ผ่าน innerHTML ไม่ก่อให้เกิดความเสี่ยงด้านความปลอดภัยที่สำคัญเนื่องจากเราเชื่อมั่นในเนื้อหาที่แสดงผลภายในแซนด์บ็อกซ์
กลไกนี้ทำให้การสร้างเทมเพลตเป็นเรื่องง่าย แต่แน่นอนว่าไม่ได้จำกัดอยู่เพียงการสร้างเทมเพลต คุณสามารถใช้แซนด์บ็อกซ์กับโค้ดใดก็ตามที่ทำงานไม่ได้ทันทีภายใต้นโยบายรักษาความปลอดภัยเนื้อหาที่เข้มงวด ในความเป็นจริง การใช้แซนด์บ็อกซ์กับคอมโพเนนต์ของส่วนขยายที่ จะ ทำงานได้อย่างถูกต้องมักจะเป็นประโยชน์เพื่อจำกัดสิทธิ์ของโปรแกรมแต่ละส่วนให้มีสิทธิ์น้อยที่สุดที่จำเป็นเพื่อให้โปรแกรมทำงานได้อย่างถูกต้อง งานนำเสนอเรื่องการเขียนเว็บแอปและส่วนขยายของ Chrome ที่ปลอดภัยจาก Google I/O 2012 มีตัวอย่างที่ดีเกี่ยวกับเทคนิคเหล่านี้ในการทำงาน และคุ้มค่าที่จะใช้เวลาดู 56 นาทีของ คุณ