Latte güvenlikle eş anlamlıdır
Latte, kritik Cross-site Scripting (XSS) güvenlik açığına karşı etkili koruması olan tek PHP şablon sistemidir. Bunu bağlama duyarlı kaçışa borçludur. Şunları ele alacağız:
- XSS güvenlik açığının ilkesi ve neden bu kadar tehlikeli olduğu
- Latte'yi XSS'e karşı savunmada bu kadar etkili kılan şey
- Twig, Blade ve benzeri şablonlarda güvenlik açıklarının ne kadar kolay oluşabileceği
Cross-site Scripting (XSS)
Cross-site Scripting (kısaca XSS), web sitelerindeki en yaygın ve en tehlikeli güvenlik açıklarından biridir. Bir saldırganın, başkasının sayfasına kötücül bir betik (malware) enjekte etmesine ve bunun da hiçbir şeyden habersiz kullanıcının tarayıcısında çalışmasına olanak tanır.
Böyle bir betik neler yapabilir? Örneğin, ele geçirilen sayfadaki herhangi bir içeriği, giriş yaptıktan sonra gösterilen hassas veriler dahil, saldırgana gönderebilir. Sayfayı değiştirebilir ya da kullanıcı adına başka istekler yapabilir. Örneğin bir web posta hizmeti olsaydı, hassas mesajları okuyabilir, gösterilen içeriği değiştirebilir ya da ayarları yeniden yapılandırabilir; örneğin gelecekteki e-postalara erişmek için tüm mesajların kopyalarının saldırganın adresine iletilmesini etkinleştirebilirdi.
Bu yüzden XSS, sürekli olarak en tehlikeli güvenlik açıkları arasında yer alır. Bir web sitesinde bir açık ortaya çıkarsa, sömürülmesini önlemek için olabildiğince çabuk giderilmelidir.
Açık nasıl ortaya çıkar?
Hata, web sayfasının üretildiği ve değişkenlerin yazdırıldığı yerde oluşur. Bir arama sayfası oluşturduğunuzu ve başında arama terimini içeren bir paragrafın şöyle yer aldığını düşünün:
echo '<p>Search results for <em>' . $search . '</em></p>';
Bir saldırgan, arama kutusuna, dolayısıyla $search değişkenine,
<script>alert("Hacked!")</script> gibi HTML kodu dahil herhangi bir dize girebilir. Çıktı sanitize
edilmediğinden, bu gösterilen sayfanın parçası olur:
<p>Search results for <em><script>alert("Hacked!")</script></em></p>
Tarayıcı, arama dizesini göstermek yerine JavaScript'i çalıştırır. Ve böylece saldırgan sayfanın denetimini ele geçirir.
Bir değişkene kod eklemenin JavaScript'i yalnızca saldırganın tarayıcısında çalıştırdığını öne sürebilirsiniz. Peki kurbana nasıl ulaşıyor? Bu açıdan bakınca birkaç XSS tipini ayırt ederiz. Arama örneğimizde yansıtılmış XSS'ten söz ediyoruz. Burada kurbanın, kötücül kodu bir parametrede taşıyan bir bağlantıya tıklamaya kandırılması gerekir:
https://example.com/?search=<script>alert("Hacked!")</script>
Kullanıcıyı bağlantıya tıklamaya yönlendirmek biraz sosyal mühendislik ister, ama pek karmaşık değildir.
Kullanıcılar, ister e-postalarda ister sosyal medyada, bağlantılara pek düşünmeden tıklar. Adresin şüpheli bir şey
içerdiği, bir URL kısaltıcıyla gizlenebilir; kullanıcı o zaman yalnızca bit.ly/xxx görür.
Ancak saklanmış XSS ya da kalıcı XSS denilen, çok daha tehlikeli ikinci bir saldırı biçimi vardır; burada saldırgan, kötücül kodu sunucuya kaydetmeyi ve böylece belirli sayfalara otomatik eklenmesini başarır.
Bir örnek, kullanıcıların yorum yazdığı sayfalardır. Saldırgan kod içeren bir gönderi yollar ve bu sunucuya kaydedilir. Sayfalar yeterince güvenli değilse, kod o zaman her ziyaretçinin tarayıcısında çalışır.
Saldırının özünün, sayfaya <script> dizesini sokmak olduğu düşünülebilir. Gerçekte JavaScript eklemenin birçok yolu
vardır. Bir HTML niteliğini kullanan bir örnek gösterelim. Görsellere alt niteliğinde gösterilen
açıklamalar eklenebilen bir fotoğraf galerisi düşünün:
echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';
Bir saldırganın açıklama olarak ustaca hazırlanmış " onload="alert('Hacked!') dizesini eklemesi yeter;
çıktı sanitize edilmezse ortaya çıkan kod şöyle görünür:
<img src="photo0145.webp" alt="" onload="alert('Hacked!')">
Enjekte edilen onload niteliği artık sayfanın parçasıdır. Tarayıcı, görsel indirilir indirilmez içindeki
kodu çalıştırır. Hacklendiniz!
XSS'e karşı nasıl korunulur?
Saldırıları kara listelerle, örneğin <script> dizesini engelleyerek saptama girişimleri yetersizdir.
İşleyen bir savunmanın temeli, sayfa içinde yazdırılan tüm verinin tutarlı biçimde sanitize edilmesidir.
Bu öncelikle, özel anlam taşıyan tüm karakterlerin karşılık gelen dizilerle değiştirilmesini kapsar; buna halk
arasında kaçış denir (dizinin ilk karakterine kaçış karakteri denir, ad da buradan gelir). Örneğin HTML metninde
< karakterinin özel bir anlamı vardır; bir etiketin başlangıcı olarak yorumlanmaması gerekiyorsa, onu
görsel olarak karşılık gelen bir diziyle, < HTML varlığıyla değiştirmemiz gerekir. Tarayıcı
o zaman küçüktür işaretini gösterir.
Verinin yazdırıldığı bağlamı ayırt etmek çok önemlidir. Çünkü dizeler farklı bağlamlarda farklı sanitize edilir. Farklı bağlamlarda farklı karakterler özel anlam taşır. Örneğin HTML metninde, HTML niteliklerinde, bazı özel elemanların içinde vb. kaçış farklıdır. Bunu birazdan ayrıntılı ele alacağız.
Sanitizasyon en iyi, dize sayfada yazdırılırken yapılır; böylece gerçekten yapıldığından ve tam olarak bir kez yapıldığından emin olunur. En iyisi, sanitizasyonun şablon sisteminin kendisi tarafından otomatik üstlenilmesidir. Çünkü sanitizasyon otomatik değilse programcı onu unutabilir. Ve tek bir gözden kaçırma, web sitesinin savunmasız olduğu anlamına gelir.
Ancak XSS yalnızca şablonlarda veri yazdırmakla ilgili değildir; uygulamanın güvenilmeyen veriyi doğru işlemesi gereken
diğer bölümlerini de ilgilendirir. Örneğin uygulamanızdaki JavaScript, güvenilmeyen veriyle bağlantılı olarak
innerHTML değil innerText veya textContent kullanmalıdır. Dizeleri JavaScript olarak
değerlendiren eval() gibi, ama aynı zamanda setTimeout() gibi fonksiyonlara ya da
setAttribute()'un onload gibi olay nitelikleriyle kullanımına özel dikkat gösterilmelidir. Bu,
şablonların kapsadığı alanın dışında kalır.
3 maddede ideal savunma:
- Verinin yazdırıldığı bağlamı tanır.
- Veriyi o bağlamın kurallarına göre sanitize eder (yani “bağlama duyarlı”).
- Bunu otomatik yapar.
Bağlama duyarlı kaçış
Bağlam sözcüğüyle tam olarak ne kastediliyor? Belgenin içinde, yazdırılan veriyi işlemek için kendi kuralları olan bir konum. Belge tipine bağlıdır (HTML, XML, CSS, JavaScript, düz metin, …) ve belirli bölümlerde farklılık gösterebilir. Örneğin bir HTML belgesinde, çok farklı kuralların geçerli olduğu birçok yer (bağlam) vardır. Sayılarına şaşırabilirsiniz. İşte ilk dördü:
<p>#text</p>
<img src="#attribute">
<textarea>#rawtext</textarea>
<!-- #comment -->
Bir HTML sayfasının varsayılan ve temel bağlamı HTML metnidir. Burada kurallar nedir? < ve
& karakterleri özel anlam taşır, bir etiketin veya varlığın başlangıcını temsil ederler; bu yüzden
onları HTML varlıklarıyla değiştirerek kaçırmamız gerekir (< → <, &
→ &).
İkinci en yaygın bağlam bir HTML niteliğinin değeridir. Metinden farkı, niteliği sınırlayan " veya
' tırnak işaretinin burada özel bir anlam taşımasıdır. Niteliğin sonu diye yorumlanmaması için varlık
olarak yazılması gerekir. Buna karşılık < karakteri bir niteliğin içinde güvenle kullanılabilir, çünkü
orada özel bir anlamı yoktur; bir etiketin ya da yorumun başlangıcı diye yorumlanamaz. Ama dikkat: HTML'de nitelik değerleri
tırnaksız da yazılabilir; o zaman bir dizi karakter özel anlam kazanır ve bu da onu ayrı bir bağlam yapar.
Şaşırabilirsiniz, ama <textarea> ve <title> elemanlarının içinde özel kurallar
geçerlidir; orada < karakterinin, ardından / gelmedikçe kaçırılması gerekmez (ama
kaçırılabilir). Bu daha çok küçük bir ayrıntıdır.
HTML yorumlarının içi ilginçleşir. Burada kaçış için HTML varlıkları kullanılmaz. Aslında hiçbir belirtim, yorumlarda kaçışın nasıl yapılacağını söylemez. Yalnızca biraz tuhaf kurallara uymanız ve içlerinde belirli karakter bileşimlerinden kaçınmanız gerekir.
Bağlamlar katmanlı da olabilir; bu, HTML'e JavaScript veya CSS gömdüğümüzde olur. Bu, bir eleman veya bir nitelik kullanılarak iki farklı şekilde yapılabilir:
<script>#js-element</script>
<img onclick="#js-attribute">
<style>#css-element</style>
<p style="#css-attribute"></p>
İki yol ve veriyi kaçırmanın iki farklı biçimi. <script> ve <style> elemanlarının
içinde, tıpkı HTML yorumlarında olduğu gibi, HTML varlıklarıyla kaçış yapılmaz. Bu elemanların içinde veri
yazdırırken yalnızca tek bir kurala uymak gerekir: metin sırasıyla </script veya </style
dizisini içermemelidir.
Buna karşılık style ve on*** niteliklerinde kaçış HTML varlıklarıyla yapılır.
Ve elbette, iç içe geçen JavaScript veya CSS'in içinde o dillerin kaçış kuralları geçerlidir. Yani
onload gibi bir niteliğin içindeki bir dize önce JS kurallarına, sonra HTML nitelik kurallarına göre
kaçırılır.
Vay be… Gördüğünüz gibi HTML, bağlamların katmanlandığı çok karmaşık bir belgedir ve veriyi tam olarak nerede (yani hangi bağlamda) yazdırdığınızı bilmeden bunu nasıl doğru yapacağınızı söyleyemezsiniz.
Bir örnek ister misiniz?
Rock'n'Roll dizesini ele alalım.
Onu HTML metninde yazdırırsanız, bu özel durumda hiçbir değişim gerekmez, çünkü dize özel anlamlı karakter içermez. Onu tek tırnakla çevrelenmiş bir HTML niteliğinin içinde yazdırırsanız durum değişir. O zaman tırnakları HTML varlıklarına kaçırmanız gerekir:
<div title='Rock'n'Roll'></div>
Bu basitti. Bağlamlar katmanlandığında, örneğin dize JavaScript'in parçası olduğunda çok daha ilginç bir durum ortaya çıkar.
Önce onu JavaScript'in kendisinde yazdıralım. Yani tırnak içine alır ve aynı anda içindeki tırnakları \
karakteriyle kaçırırız:
'Rock\'n\'Roll'
Kodun bir şey yapması için bir fonksiyon çağrısı da ekleyebiliriz:
alert('Rock\'n\'Roll');
Bu kodu <script> kullanarak bir HTML belgesine eklersek, yasak </script dizisini
içermediğinden başka bir değişiklik gerekmez:
<script> alert('Rock\'n\'Roll'); </script>
Ancak onu bir HTML niteliğine eklemek isteseydik, yine de tırnakları HTML varlıklarına kaçırmamız gerekirdi:
<div onclick='alert('Rock\'n\'Roll')'></div>
Ama iç içe geçen bağlam yalnızca JS veya CSS olmak zorunda değildir. Yaygın olarak bir URL de olur. URL'lerdeki
parametreler, özel anlamlı karakterlerin % ile başlayan dizilere dönüştürülmesiyle
kaçırılır. Örnek:
https://example.org/?a=Jazz&b=Rock%27n%27Roll
Ve bu dizeyi bir nitelikte yazdırdığımızda, yine bu bağlama göre kaçış uygularız ve & yerine
& koyarız:
<a href="https://example.org/?a=Jazz&b=Rock%27n%27Roll">
Buraya kadar okuduysanız, tebrikler, kapsamlıydı. Artık bağlamların ve kaçışın ne olduğunu iyi anlıyorsunuz. Ve bunun karmaşık olmasından endişelenmenize gerek yok. Latte bunu sizin için otomatik yapar.
Latte ile saf sistemler
Bir HTML belgesinde nasıl doğru kaçırılacağını ve bağlam bilgisinin, yani veriyi yazdırdığımız yerin, ne kadar önemli olduğunu gösterdik. Başka bir deyişle, bağlama duyarlı kaçışın nasıl çalıştığını. Bu, XSS'e karşı işleyen bir savunmanın zorunlu önkoşulu olsa da, bunu yapabilen tek PHP şablon sistemi Latte'dir.
Bugün tüm sistemler otomatik kaçışa sahip olduklarını söylerken bu nasıl mümkün olur? Bağlam bilgisi olmadan otomatik kaçış biraz yanılgıdır ve yanlış bir güvenlik duygusu yaratır.
Twig, Laravel Blade gibi şablon sistemleri şablonda hiçbir HTML yapısı görmez. Bu yüzden bağlamları da görmezler. Latte'ye kıyasla kör ve saftırlar. Yalnızca kendi etiketlerini işlerler; geri kalan her şey onlar için önemsiz bir karakter akışıdır:
░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
- metinde: <span>{{ foo }}</span>
- etikette: <span {{ foo }} ></span>
- nitelikte: <span title='{{ foo }}'></span>
- tırnaksız nitelikte: <span title={{ foo }}></span>
- URL içeren nitelikte: <a href="{{ foo }}"></a>
- JavaScript içeren nitelikte: <img onload="{{ foo }}">
- CSS içeren nitelikte: <span style="{{ foo }}"></span>
- JavaScript'te: <script>var = {{ foo }}</script>
- CSS'te: <style>body { content: {{ foo }}; }</style>
- yorumda: <!-- {{ foo }} -->
Saf sistemler < > & ' " karakterlerini yalnızca mekanik olarak HTML varlıklarına dönüştürür; bu
çoğu kullanım durumunda geçerli bir kaçış yöntemi olsa da her zaman yeterli olmaktan uzaktır. Böylece, aşağıda
göstereceğimiz gibi, çeşitli güvenlik açıklarının oluşmasını saptayamaz ya da önleyemezler.
Latte, şablonu tıpkı sizin gibi görür. HTML'i, XML'i anlar, etiketleri, nitelikleri vb. tanır. Ve bu sayede tek tek bağlamları ayırt eder ve veriyi buna göre işler. Böylece kritik Cross-site Scripting açığına karşı gerçekten etkili bir koruma sunar.
░░░░░░░░░░░<span>{$foo}</span>
░░░░░░░░░░<span {$foo} ></span>
░░░░░░░░░░░░░░░░<span title='{$foo}'></span>
░░░░░░░░░░░░░░░░░░░░░░░░░<span title={$foo}></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<a href="{$foo}"></a>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<img onload="{$foo}">
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<span style="{$foo}"></span>
░░░░░░░░░░░░░░░░░░<script>░░░░░░{$foo}</script>
░░░░░░░░░░<style>░░░░░░░░░░░░░░░░{$foo}░░░</style>
░░░░░░░░░░░░░░<!--░{$foo}░-->
- metinde: <span>{$foo}</span>
- etikette: <span {$foo} ></span>
- nitelikte: <span title='{$foo}'></span>
- tırnaksız nitelikte: <span title={$foo}></span>
- URL içeren nitelikte: <a href="{$foo}"></a>
- JavaScript içeren nitelikte: <img onload="{$foo}">
- CSS içeren nitelikte: <span style="{$foo}"></span>
- JavaScript'te: <script>var = {$foo}</script>
- CSS'te: <style>body { content: {$foo}; }</style>
- yorumda: <!-- {$foo} -->
Canlı gösterim
Solda Latte'deki şablonu, sağda üretilen HTML kodunu görüyorsunuz. $text değişkeni birkaç kez, her
seferinde biraz farklı bir bağlamda yazdırılıyor. Ve dolayısıyla biraz farklı kaçırılıyor. Şablon kodunu kendiniz
düzenleyebilirsiniz, örneğin değişkenin içeriğini değiştirebilirsiniz. Deneyin:
Harika değil mi! Latte, bağlama duyarlı kaçışı otomatik yapar, böylece programcı:
- nerede nasıl kaçırılacağını düşünmek ya da bilmek zorunda değildir
- hata yapamaz
- kaçışı unutamaz
Bunlar, Latte'nin yazdırırken ayırt ettiği ve veri işleyişini ona göre uyarladığı bağlamların hepsi bile değil. Şimdi diğer ilginç durumları gözden geçireceğiz.
Saf sistemler nasıl hacklenir
Birkaç pratik örnekle, bağlam ayrımının ne kadar önemli olduğunu ve saf şablon sistemlerinin, Latte'nin aksine, XSS'e karşı neden yeterli koruma sağlamadığını göstereceğiz. Örneklerde saf sistemin temsilcisi olarak Twig'i kullanacağız, ama aynısı başka sistemler için de geçerlidir.
Nitelikteki açık
Yukarıda gösterdiğimiz gibi, bir HTML niteliği kullanarak sayfaya kötücül kod enjekte etmeyi deneyelim. Twig'de bir görsel render eden bir şablonumuz olsun:
<img src={{ imageFile }} alt={{ imageAlt }}>
Nitelik değerlerinin çevresinde tırnak olmadığına dikkat edin. Kodlayıcı onları unutmuş olabilir, bu olur. Örneğin React'te kod böyle, tırnaksız yazılır ve diller arasında geçiş yapan bir kodlayıcı tırnakları kolayca unutabilir.
Saldırgan, görsel açıklaması olarak ustaca hazırlanmış foo onload=alert('Hacked!') dizesini ekler. Twig'in
bir değişkenin HTML metin akışında mı, bir niteliğin içinde mi, bir HTML yorumunda mı yazdırıldığını
belirleyemediğini; kısacası bağlamları ayırt etmediğini zaten biliyoruz. Ve yalnızca < > & ' "
karakterlerini mekanik olarak HTML varlıklarına dönüştürür. Yani ortaya çıkan kod şöyle görünecektir:
<img src=photo0145.webp alt=foo onload=alert('Hacked!')>
Bir güvenlik açığı oluştu!
Sahte bir onload niteliği sayfanın parçası oldu ve tarayıcı, görseli indirdikten hemen sonra onu
çalıştırır.
Şimdi Latte'nin aynı şablonu nasıl ele aldığına bakalım:
<img src={$imageFile} alt={$imageAlt}>
Latte şablonu tıpkı sizin gibi görür. Twig'in aksine HTML'i anlar ve değişkenin, tırnak içine alınmamış bir niteliğin değeri olarak yazdırıldığını bilir. Bu yüzden tırnakları ekler. Saldırgan aynı açıklamayı eklediğinde ortaya çıkan kod şöyle görünecektir:
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
Latte XSS'i başarıyla önledi.
JavaScript'te değişken yazdırma
Bağlama duyarlı kaçış sayesinde PHP değişkenlerini JavaScript'in içinde doğal olarak kullanmak mümkündür.
<p onclick="alert({$movie})">{$movie}</p>
<script>var movie = {$movie};</script>
$movie değişkeni 'Amarcord & 8 1/2' dizesini içeriyorsa aşağıdaki çıktı üretilir. HTML
içindeki kaçışın JavaScript içindekinden, onclick niteliğindekinin ise ikisinden de farklı olduğuna
dikkat edin:
<p onclick="alert("Amarcord & 8 1/2")">Amarcord & 8 1/2</p>
<script>var movie = "Amarcord & 8 1/2";</script>
Bağlantı denetimi
Latte, src, href, action, formaction gibi URL niteliklerinde ya da
<object> elemanındaki data niteliğinde kullanılan bir değişkenin güvenli bir URL içerip
içermediğini otomatik denetler. Yaygın protokollere (http, https, ftp,
mailto, tel, sms) ve göreli URL'lere izin verirken, javascript: gibi olası
tehlikeli olanları engeller.
{var $link = 'javascript:attack()'}
<a href={$link}>click here</a>
Şunu üretir:
<a href="">click here</a>
Denetim, nocheck filtresiyle kapatılabilir.
İstemci tarafı framework'ler
Vue veya Angular gibi framework'ler, sayfanın bir bölümünü kendi şablonları sayar ve içindeki çift küme
parantezlerini {{ ... }} bir ifade olarak yorumlar. Bir saldırgan böyle bir diziyi yazdırılan bir değişkene
sokmayı başarırsa, sunucu tarafında hiçbir XSS olmasa bile framework onu sayfa yüklendikten sonra tarayıcıda
değerlendirir.
Latte bunu önler. HTML metnine yazdırırken {{ çiftini {<!-- -->{ haline getirir; boş yorum
okuyucu için görünmezdir, yine de framework'ün kendi etiketini tanımasını engeller. { karakterinin başka her
geçişi { varlığıyla değiştirilir (nitelik değerlerinin içinde her { böyle
kaçırılır).
{var $query = 'Hi {{ constructor.constructor("alert(1)")() }}'}
<h1>Search: {$query}</h1>
Şunu üretir:
<h1>Search: Hi {<!-- -->{ constructor.constructor("alert(1)")() }}</h1>
Koruma otomatik çalışır ve yapılandırma gerektirmez.
Latte'nin sınırları
Latte, uygulamanın tamamı için eksiksiz bir XSS koruması değildir. Latte kullanırken güvenliği düşünmeyi bırakırsanız üzülürüz. Latte'nin amacı, bir saldırganın sayfanın yapısını değiştirememesini, HTML elemanlarını veya niteliklerini sahteleyememesini sağlamaktır. Ama yazdırılan verinin içerik doğruluğunu denetlemez. JavaScript davranışının doğruluğunu da. Bu, şablon sisteminin yetki alanının dışında kalır. Verinin, özellikle kullanıcının girdiği ve dolayısıyla güvenilmeyen verinin doğruluğunu doğrulamak, programcının önemli bir görevidir.
Tek tek bağlamlarda kaçış bilginizi sınamak ister misiniz? XSS güvenlik açığı sınavını deneyin.