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 taşınarak ayrı JavaScript dosyalarına yerleştirilmeli, satır içi etkinlik işleyiciler addEventListener kullanacak şekilde dönüştürülmeli ve eval() devre dışı bırakılmalıdır.
Ancak, ç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ı, bu uygulama stiline özellikle yatkındır. Bazı çerçeveler (ör. Angular.js) CSP'yi kutudan çıktığı haliyle desteklese de birçok popüler çerçeve henüz uzantıların eval'siz dünyasıyla uyumlu bir mekanizmaya güncellenmemiştir. Bu nedenle, söz konusu işlev için desteğin kaldırılması geliştiriciler açısından beklenenden daha sorunlu oldu.
Bu belgede, güvenliği tehlikeye atmadan bu kitaplıkları projelerinize dahil etmek için güvenli bir mekanizma olarak korumalı alan tanıtılmaktadır.
Neden korumalı alan?
eval, uzantının yüksek izinli ortamındaki her şeye erişebilen kodlar yürüttüğünden uzantı içinde tehlikelidir. Kullanıcının güvenliğini ve gizliliğini ciddi şekilde etkileyebilecek bir dizi güçlü browser.* API mevcuttur. Basit veri sızdırma, endişeleneceğimiz en küçük konudur.
Sunulan çözüm, eval'nın uzantının verilerine veya uzantının yüksek değerli API'lerine erişmeden kodu yürütebileceği bir sanal alandır. Veri, API veya sorun yok.
Bunu, uzantı paketindeki belirli HTML dosyalarını korumalı alan olarak listeleyerek yaparız.
Koruma alanına alınmış bir sayfa her yüklendiğinde benzersiz bir kaynağa taşınır ve browser.* API'lerine erişimi engellenir. Bu korumalı alanı iframe aracılığıyla uzantımıza yüklersek ona mesajlar iletebilir, bu mesajlar üzerinde bir şekilde işlem yapmasına izin verebilir ve bize sonucu geri iletmesini bekleyebiliriz. Bu basit mesajlaşma mekanizması, eval destekli kodu uzantımızın iş akışına güvenli bir şekilde dahil etmek için ihtiyacımız olan her şeyi sağlar.
Korumalı alan oluşturma ve kullanma
Doğrudan kod yazmaya başlamak istiyorsanız sandbox örneği uzantısını alın ve çalışmaya başlayın. Bu uzantı, Handlebars şablon kitaplığı üzerine kurulu küçük bir mesajlaşma API'sinin çalışan bir örneğidir ve başlamak için ihtiyacınız olan her şeyi size sunar. Daha fazla açıklama isteyenler için bu örneği birlikte inceleyelim.
Manifestteki dosyaları listeleme
Bir sanal alan içinde çalıştırılması gereken her dosya, sandbox özelliği eklenerek uzantı manifestinde listelenmelidir. Bu önemli adımı unutmamaya dikkat edin. Korumalı alan dosyanızın manifestte listelendiğinden emin olmak için iki kez kontrol edin. Bu örnekte, "sandbox.html" adlı dosyayı akıllıca sanal ortamda test ediyoruz. Manifest girişi aşağıdaki gibi görünür:
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
Korumalı alandaki dosyayı yükleme
Koruma alanına alınmış dosyayla ilginç bir işlem yapabilmek için dosyayı, uzantının kodu tarafından ele alınabileceği bir bağlamda yüklememiz gerekir. Burada sandbox.html, iframe kullanılarak bir uzantı sayfasına yüklenmiştir. Sayfanın JavaScript dosyası, tarayıcı işlemi tıklandığında sayfadaki iframe öğesini bulup contentWindow üzerinde postMessage() işlevini çağırarak sanal ortama mesaj gönderen bir kod içeriyor. Mesaj, context, templateName ve command olmak üzere üç özellik içeren bir nesnedir. Birazdan context ve command konularına değineceğiz.
service-worker.js:
browser.action.onClicked.addListener(() => {
browser.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 yapmak
sandbox.html yüklendiğinde Handlebars kitaplığını yükler ve Handlebars'ın önerdiği şekilde satır içi bir şablon oluşturup derler:
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 kullanmasına rağmen her şey tam olarak beklendiği gibi çalışır ve templates['hello'] içinde derlenmiş bir şablon elde ederiz.
Sonucu geri iletme
Uzantı sayfasından gelen komutları kabul eden bir ileti dinleyici ayarlayarak bu şablonu kullanıma sunacağız. Ne yapılması gerektiğini belirlemek için command parametresini kullanacağız (yalnızca oluşturmaktan daha fazlasını yapabileceğinizi düşünün; örneğin, şablon oluşturma). Belki de bunları bir şekilde yönetmek istiyorsunuzdur. Bu durumda context, oluşturma için doğrudan şablona aktarılır. Oluşturulan HTML, uzantının daha sonra yararlı bir işlem yapabilmesi için uzantı sayfasına geri iletilir:
<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 döndüğümüzde bu mesajı alırız ve html
bize iletilen verilerle ilginç bir işlem yaparız. Bu durumda, yalnızca bildirim aracılığıyla yansıtacağı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 üzerinden eklemek, sanal alan içinde 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 elbette şablon oluşturmayla sınırlı değildir. Sıkı bir İçerik Güvenliği Politikası kapsamında kutudan çıktığı gibi çalışmayan tüm kodlar korumalı alana alınabilir. Aslında, programınızın her bir parçasını düzgün şekilde yürütmek için gereken en küçük ayrıcalıklar kümesiyle sınırlamak amacıyla, uzantılarınızın doğru şekilde çalışacak bileşenlerini korumalı alana almak genellikle yararlıdır. Google I/O 2012'deki Writing Secure Web Apps and Chrome Extensions (Güvenli Web Uygulamaları ve Chrome Uzantıları Yazma) sunumunda bu tekniklerin uygulamadaki bazı iyi örnekleri verilmektedir ve 56 dakikanızı ayırmaya değer.