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