ระบบส่วนขยายของ Chrome บังคับใช้นโยบายรักษาความปลอดภัยเนื้อหา (CSP) เริ่มต้นที่ค่อนข้างเคร่งครัด
ข้อจำกัดของนโยบายนั้นไม่ซับซ้อน: ต้องย้ายสคริปต์ออกนอกบรรทัดไปยังไฟล์ JavaScript แยกต่างหาก ต้องแปลงเครื่องจัดการเหตุการณ์ในบรรทัดไปใช้ addEventListener
และปิดใช้ eval()
อย่างไรก็ตาม เราทราบว่าไลบรารีมากมายใช้โครงสร้าง 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 ลงในหน้า
ส่วนขยายผ่าน 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
เครื่องมือจะโหลดไลบรารีของ Handlebar รวมถึงสร้างและคอมไพล์เทมเพลตอินไลน์ในลักษณะที่แฮนเดิลแนะนำ
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 ของส่วนขยายได้อย่างปลอดภัย การแทรก URL ผ่าน innerHTML
ไม่ได้ก่อให้เกิดความเสี่ยงด้านความปลอดภัยมากนักเนื่องจากเราเชื่อถือเนื้อหาที่แสดงภายในแซนด์บ็อกซ์
กลไกนี้ทำให้การกำหนดเทมเพลตเป็นเรื่องง่าย แต่ไม่ได้จำกัดเพียงเทมเพลต คุณอาจแซนด์บ็อกซ์โค้ดใดๆ ที่ทำงานได้ไม่ดีภายใต้นโยบายรักษาความปลอดภัยเนื้อหาที่เข้มงวด อันที่จริงมักมีประโยชน์สำหรับคอมโพเนนต์แซนด์บ็อกซ์ของส่วนขยายที่จะทำงานได้อย่างถูกต้องเพื่อจำกัดแต่ละส่วนของโปรแกรมให้เหลือน้อยที่สุดเท่าที่จำเป็นต่อการดำเนินการอย่างถูกต้อง งานนำเสนอ การเขียนเว็บแอปที่ปลอดภัยและส่วนขยาย Chrome จาก Google I/O 2012 เป็นตัวอย่างที่ดีของการใช้งานเทคนิคเหล่านี้ และใช้เวลา 56 นาที