Hệ thống tiện ích của Chrome thực thi một Chính sách bảo mật nội dung (CSP) mặc định khá nghiêm ngặt.
Các hạn chế của chính sách rất rõ ràng: tập lệnh phải được di chuyển ngoài dòng thành các tệp JavaScript riêng biệt, trình xử lý sự kiện cùng dòng phải được chuyển đổi để sử dụng addEventListener
và eval()
bị tắt.
Tuy nhiên, chúng tôi nhận thấy rằng nhiều thư viện sử dụng cấu trúc giống eval()
và eval
(chẳng hạn như new Function()
) để tối ưu hoá hiệu suất và dễ biểu thức. Các thư viện tạo mẫu đặc biệt dễ gặp phải kiểu triển khai này. Mặc dù một số khung (như Angular.js) hỗ trợ CSP ngay từ đầu, nhưng nhiều khung phổ biến vẫn chưa cập nhật lên cơ chế tương thích với
thế giới không có eval
của tiện ích. Do đó, việc ngừng hỗ trợ chức năng đó cho thấy nhà phát triển có nhiều vấn đề hơn dự kiến.
Tài liệu này giới thiệu hộp cát như một cơ chế an toàn để đưa những thư viện này vào dự án của bạn mà không ảnh hưởng đến tính bảo mật.
Tại sao nên dùng hộp cát?
eval
nguy hiểm bên trong một tiện ích vì mã mà tiện ích này thực thi có quyền truy cập vào mọi nội dung trong môi trường cấp quyền cao của tiện ích đó. Một số API chrome.*
mạnh mẽ hiện có có thể ảnh hưởng nghiêm trọng đến tính bảo mật và quyền riêng tư của người dùng; việc đánh cắp dữ liệu đơn giản là vấn đề ít phải lo lắng nhất.
Giải pháp được cung cấp là một hộp cát, trong đó eval
có thể thực thi mã mà không cần quyền truy cập vào dữ liệu của tiện ích hoặc các API có giá trị cao của tiện ích. Không có dữ liệu, không có API, không sao cả.
Chúng tôi thực hiện việc này bằng cách liệt kê các tệp HTML cụ thể bên trong gói tiện ích ở dạng hộp cát.
Mỗi khi một trang hộp cát được tải, trang đó sẽ được chuyển đến một nguồn gốc duy nhất và sẽ bị từ chối quyền truy cập vào các API chrome.*
. Nếu tải trang có hộp cát này vào tiện ích qua iframe
, chúng ta có thể
truyền thông báo cho trang đó, để trang hoạt động dựa trên các thông báo đó theo cách nào đó và chờ trang trả về cho chúng tôi
kết quả. Cơ chế thông báo đơn giản này cung cấp cho chúng ta mọi thứ cần thiết để đưa mã dựa trên eval
vào quy trình công việc của tiện ích một cách an toàn.
Tạo và sử dụng hộp cát
Nếu bạn muốn đi thẳng vào mã, hãy lấy tiện ích mẫu trong hộp cát và bắt đầu. Đây là một ví dụ hoạt động về API nhắn tin nhỏ được xây dựng trên thư viện tạo mẫu Thanh điều khiển và nó sẽ cung cấp cho bạn mọi thứ bạn cần để bắt đầu. Đối với những ai muốn hiểu rõ hơn, hãy cùng xem qua mẫu đó ở đây.
Liệt kê tệp trong tệp kê khai
Mỗi tệp cần chạy trong một hộp cát phải được liệt kê trong tệp kê khai tiện ích bằng cách thêm thuộc tính sandbox
. Đây là một bước quan trọng và bạn dễ quên, vì vậy, hãy kiểm tra kỹ để đảm bảo tệp hộp cát của bạn được liệt kê trong tệp kê khai. Trong mẫu này, chúng ta tạo hộp cát cho tệp được đặt tên khéo léo là "sandbox.html". Mục nhập tệp kê khai sẽ có dạng như sau:
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
Tải tệp hộp cát
Để thực hiện một hoạt động thú vị với tệp hộp cát, chúng ta cần tải tệp đó trong ngữ cảnh có thể xử lý bằng mã của tiện ích. Ở đây, hộp cát.html đã được tải vào
trang tiện ích thông qua iframe
. Tệp javaScript của trang chứa mã gửi thông báo vào hộp cát bất cứ khi nào thao tác trình duyệt được nhấp bằng cách tìm iframe
trên trang và gọi postMessage()
trên contentWindow
của nó. Thông báo là một đối tượng chứa 3 thuộc tính: context
, templateName
và command
. Chúng ta sẽ sớm tìm hiểu về context
và 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, '*');
});
Làm việc gì đó nguy hiểm
Khi được tải, sandbox.html
sẽ tải thư viện Tay cầm, đồng thời tạo và biên dịch một mẫu cùng dòng theo cách Tay cầm đề xuất:
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>
Không sao cả! Mặc dù Handlebars.compile
kết thúc bằng new Function
, nhưng mọi thứ vẫn hoạt động chính xác như dự kiến và chúng tôi vẫn có một mẫu đã biên dịch trong templates['hello']
.
Trả về kết quả
Chúng tôi sẽ cung cấp mẫu này cho bạn sử dụng bằng cách thiết lập trình nghe thông báo chấp nhận các lệnh
từ trang tiện ích. Chúng ta sẽ sử dụng command
được truyền vào để xác định việc cần làm (bạn có thể tưởng tượng việc này không chỉ là kết xuất đồ hoạ; có thể là tạo mẫu?) Bạn có thể cần quản lý chúng theo cách nào đó?) và context
sẽ được chuyển trực tiếp vào mẫu để kết xuất. HTML đã kết xuất sẽ được trả về trang tiện ích để tiện ích có thể hữu ích sau này:
<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>
Quay lại trang tiện ích, chúng ta sẽ nhận được thông báo này và thực hiện một hành động thú vị với dữ liệu html
mà chúng tôi đã truyền. Trong trường hợp này, chúng tôi sẽ chỉ đọc lại thông qua thông báo, nhưng bạn hoàn toàn có thể sử dụng HTML này một cách an toàn trong giao diện người dùng của tiện ích. Việc chèn đoạn mã qua innerHTML
không gây ra rủi ro bảo mật đáng kể vì chúng tôi tin tưởng nội dung kết xuất trong hộp cát.
Cơ chế này giúp việc tạo mẫu trở nên đơn giản, nhưng tất nhiên không chỉ bị giới hạn ở việc tạo mẫu. Bất kỳ mã nào không hoạt động tốt theo Chính sách bảo mật nội dung nghiêm ngặt đều có thể được tạo hộp cát. Trên thực tế, các thành phần hộp cát của tiện ích sẽ chạy đúng cách để hạn chế từng phần của chương trình ở nhóm đặc quyền nhỏ nhất cần thiết để chương trình thực thi đúng cách. Bài trình bày Viết ứng dụng web an toàn và tiện ích Chrome tại Google I/O 2012 đưa ra một số ví dụ điển hình về kỹ thuật này trong thực tế và bạn sẽ mất 56 phút để tham dự.