Chrome'un uzantı sistemi oldukça katı bir varsayılan İçerik Güvenliği Politikası (İGP) uygular.
Politika kısıtlamaları basittir: Komut dosyası satır dışına, ayrı JavaScript dosyalarına taşınmalıdır, satır içi etkinlik işleyicileri addEventListener
kullanacak şekilde dönüştürülmelidir ve eval()
devre dışı bırakılmalıdır.
Bununla birlikte, çeşitli kitaplıkların performans optimizasyonu ve ifade kolaylığı için eval()
ve eval
benzeri yapıları (ör. new Function()
) kullandığını biliyoruz. Şablon kitaplıkları özellikle bu tür bir uygulamaya eğilimlidir. Bazıları (ör. Angular.js) CSP'yi kutudan çıkar çıkmaz desteklese de popüler çerçevelerin çoğu henüz uzantıların eval
'siz dünyasıyla uyumlu bir mekanizmaya güncellenmedi. Bu nedenle, bu işlev için desteğin kaldırılması geliştiriciler için beklenenden daha sorunlu oldu.
Bu dokümanda, bu kitaplıkları güvenlikten ödün vermeden projelerinize dahil etmek için güvenli bir mekanizma olarak korumalı alan kullanımı tanıtılmaktadır.
Korumalı alan neden gerekli?
eval
, yürüttüğü kod uzantının yüksek izinli ortamındaki her şeye erişebildiğinden uzantı içinde tehlikelidir. Kullanıcıların güvenliğini ve gizliliğini ciddi şekilde etkileyebilecek çok sayıda güçlü chrome.*
API mevcuttur. Basit veri sızıntıları, endişelerimiz arasında en az önemli olanıdır.
Sunulan çözüm, eval
'nin uzantının verilerine veya yüksek değerli API'lerine erişmeden kod yürütebileceği bir korumalı alan sağlar. Veri yok, API yok, sorun yok.
Bunu, uzantı paketindeki belirli HTML dosyalarını korumalı alan olarak listelememiz sayesinde yapıyoruz.
Korumalı alan sayfası her yüklendiğinde benzersiz bir kaynağa taşınır ve chrome.*
API'lerine erişimi reddedilir. Bu korumalı alan sayfasını bir iframe
aracılığıyla uzantımıza yüklersek ona mesaj iletebilir, bu mesajlar üzerinde bir şekilde işlem yapmasına izin verebilir ve bize bir sonuç döndürmesini bekleyebiliriz. Bu basit mesajlaşma mekanizması, uzantımızın iş akışına eval
tarafından yönlendirilen kodu güvenli bir şekilde dahil etmek için ihtiyacımız olan her şeyi bize sağlar.
Korumalı alan oluşturma ve kullanma
Hemen koda dalmak istiyorsanız korumalı alan örnek uzantısını indirip işe başlayın. Handlebars şablon kitaplığının üzerine inşa edilmiş küçük bir mesajlaşma API'sinin çalışan bir örneği olan bu uzantı, işe başlamak için ihtiyacınız olan her şeyi size sunar. Konuyu biraz daha ayrıntılı bir şekilde öğrenmek isteyenler için bu örneği birlikte inceleyelim.
Manifest dosyasında dosyaları listeleme
Bir korumalı alanda çalıştırılması gereken her dosya, sandbox
özelliği eklenerek uzantı manifestinde listelenmelidir. Bu önemli bir adımdır ve kolayca unutulabilir. Bu nedenle, korumalı alan dosyanızı manifest dosyasında listelediğinizden emin olun. Bu örnekte, "sandbox.html" olarak adlandırılan dosyayı korumalı alana alıyoruz. Manifest girişi şu şekilde görünür:
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
Korumalı alan dosyası yükleme
Korumalı alan dosyası ile ilginç bir şey yapmak için dosyayı, uzantının kodu tarafından erişilebileceği bir bağlamda yüklememiz gerekir. Burada, sandbox.html bir iframe
aracılığıyla uzantı sayfasına yüklendi. Sayfanın JavaScript dosyası, tarayıcı işlemi tıklandığında sayfada iframe
bulunarak ve contentWindow
üzerinde postMessage()
çağrılarak korumalı alana mesaj gönderen kod içerir. Mesaj, context
, templateName
ve command
olmak üzere üç özellik içeren bir nesnedir. context
ve command
konularını birazdan ele alacağız.
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, '*');
});
Tehlikeli bir şey yapma
sandbox.html
yüklendiğinde Handlebars kitaplığı yüklenir ve Handlebars'ın önerdiği şekilde satır içi bir şablon oluşturulup derlenir:
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>
Bu işlem başarısız olmaz. Handlebars.compile
, new Function
'u kullansa da işler tam olarak beklendiği gibi işler ve templates['hello']
biçiminde derlenmiş bir şablon elde ederiz.
Sonucu geri gönderme
Uzantı sayfasından komutları kabul eden bir mesaj dinleyicisi ayarlayarak bu şablonu kullanıma sunacağız. Ne yapılması gerektiğini belirlemek için iletilen command
değerini kullanırız (yalnızca oluşturmanın ötesinde, şablon oluşturmayı da düşünebilirsiniz). Belki bir şekilde yönetiyorsunuzdur?), context
doğrudan şablona aktarılır ve oluşturulur. Oluşturulan HTML, uzantının daha sonra bu HTML ile yararlı bir işlem yapabilmesi için uzantı sayfasına geri gönderilir:
<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>
Uzantı sayfasına geri döndüğümüzde bu mesajı alır ve bize iletilen html
verileri kullanarak ilginç bir şey yaparız. Bu durumda, bu durumu yalnızca bir bildirim aracılığıyla yansıtırız. Ancak bu HTML'yi uzantının kullanıcı arayüzünün bir parçası olarak güvenli bir şekilde kullanmak tamamen mümkündür. innerHTML
aracılığıyla eklemek, korumalı alanda oluşturulan içeriğe güvendiğimiz için önemli bir güvenlik riski oluşturmaz.
Bu mekanizma, şablon oluşturmayı kolaylaştırır ancak şablon oluşturmayla sınırlı değildir. Katı bir İçerik Güvenliği Politikası altında kutudan çıkar çıkmaz çalışmayan tüm kodlar korumalı alana yerleştirilebilir. Aslında, programınızın her bir parçasını düzgün şekilde yürütülmesi için gereken en küçük ayrıcalıklar grubuyla kısıtlamak amacıyla, uzantılarınızın doğru şekilde çalışacak bileşenlerini korumalı alana yerleştirmek genellikle yararlıdır. Google I/O 2012'deki Güvenli Web Uygulamaları ve Chrome Uzantıları Yazma sunumu, bu tekniklerin kullanıma yönelik bazı iyi örneklerini sunar ve 56 dakikanızı ayırmaya değerdir.