Test: XSS Güvenlik Açığından Kendinizi Koruyabilir misiniz?

2 yıl önce Yazan David Grudl  

Güvenlik testinde bilginizi sınayın! Bir saldırganın HTML sayfasının kontrolünü ele geçirmesini engelleyebilir misiniz?

Tüm görevlerde aynı soruyu çözeceksiniz: XSS güvenlik açığı oluşmaması için $str değişkenini bir HTML sayfasında nasıl doğru bir şekilde yazdırırsınız? Savunmanın temeli kaçış (escaping) işlemidir, bu da özel anlamı olan karakterleri karşılık gelen dizilerle değiştirmek anlamına gelir. Örneğin, < karakterinin özel bir anlamı olduğu (bir etiketin başlangıcını işaret eder) HTML metnine bir dize yazdırırken, onu HTML varlığı &lt; ile değiştiririz ve tarayıcı < sembolünü doğru bir şekilde görüntüler.

Dikkatli olun, çünkü XSS güvenlik açığı çok ciddidir. Bir saldırganın sayfanın veya hatta kullanıcı hesabının kontrolünü ele geçirmesine neden olabilir. Bol şans ve HTML sayfasını güvende tutmayı başarmanız dileğiyle!

İlk Üç Soru

Birinci, ikinci ve üçüncü örnekte hangi karakterlerin ve nasıl işlenmesi gerektiğini belirtin:

1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>

Eğer çıktı hiçbir şekilde işlenmezse, görüntülenen sayfanın bir parçası haline gelir. Bir saldırgan değişkene 'foo" onclick="evilCode()' dizesini sokarsa ve çıktı işlenmezse, öğeye tıklandığında kodunun çalışmasına neden olur:

$str = 'foo" onclick="evilCode()'
❌ işlenmemiş: <input value="foo" onclick="evilCode()">
✅ işlenmiş:  <input value="foo&quot; onclick=&quot;evilCode()">

Bireysel örneklerin çözümü:

  1. < ve & karakterleri HTML etiketi ve varlığının başlangıcını temsil eder, bunları &lt; ve &amp; ile değiştiririz
  2. " ve & karakterleri nitelik değerinin sonunu ve HTML varlığının başlangıcını temsil eder, bunları &quot; ve &amp; ile değiştiririz
  3. ' ve & karakterleri nitelik değerinin sonunu ve HTML varlığının başlangıcını temsil eder, bunları &apos; ve &amp; ile değiştiririz

Her doğru cevap için bir puan alırsınız. Elbette, her üç durumda da diğer karakterleri varlıklarla değiştirmek mümkündür, bu bir sorun yaratmaz, ancak gerekli değildir.

Soru No. 4

Devam ediyoruz. Bu bağlamda değişkeni yazdırırken hangi karakterlerin değiştirilmesi gerekiyor?

<input value=<?= $str ?>>

Çözüm: Gördüğünüz gibi, burada tırnak işaretleri eksik. En kolayı sadece tırnak işaretlerini eklemek ve ardından önceki sorudaki gibi kaçış işlemi yapmaktır. İkinci bir çözüm de vardır, o da dizedeki boşluğu ve etiket içinde özel anlamı olan tüm karakterleri, yani >, /, = ve bazı diğerleri HTML varlıklarıyla değiştirmektir.

Soru No. 5

Şimdi işler ilginçleşmeye başlıyor. Bu bağlamda hangi karakterlerin işlenmesi gerekiyor:

<script>
	let foo = '<?= $str ?>';
</script>

Çözüm: <script> etiketi içinde, kaçış kurallarını JavaScript belirler. HTML varlıkları burada kullanılmaz, ancak özel bir kural geçerlidir. Peki hangi karakterleri kaçırıyoruz? JavaScript dizesi içinde, onu sınırlayan ' karakterini elbette ters eğik çizgiyle kaçırırız, yani onu \' ile değiştiririz. JavaScript çok satırlı dizeleri desteklemediğinden (yalnızca template literal olarak), satır sonu karakterlerini de kaçırmamız gerekir. Ancak dikkatli olun, normal \n ve \r karakterlerine ek olarak, JavaScript unicode karakterleri \u2028 ve \u2029'u da satır sonu olarak kabul eder, bunları da kaçırmamız gerekir. Ve son olarak belirtilen özel kural: dizede </script bulunmamalıdır. Bu, örneğin <\/script ile değiştirilerek önlenebilir.

Eğer bunu biliyorsanız, tebrikler.

Soru No. 6

Aşağıdaki bağlam sadece önceki bir varyasyon gibi görünüyor. Sizce işleme farklı olacak mı?

<p onclick="foo('<?= $str ?>')"></p>

Çözüm: Burada yine JavaScript dizelerindeki kaçış kuralları geçerlidir, ancak HTML varlıklarının kullanılmadığı önceki bağlamın aksine, burada tam tersi kaçış işlemi yapılır. Yani, önce JavaScript dizesini ters eğik çizgilerle kaçırırız ve ardından özel anlamı olan karakterleri (" ve &) HTML varlıklarıyla değiştiririz. Dikkat, doğru sıra önemlidir.

Gördüğünüz gibi, aynı JavaScript literali <script> öğesinde farklı, bir nitelikte farklı kodlanabilir!

Soru No. 7

JavaScript'ten HTML'ye geri dönelim. Yorum içinde hangi karakterleri ve nasıl değiştirmemiz gerekiyor?

<!-- <?= $str ?> -->

Çözüm: HTML (ve XML) yorumu içinde, <, &, " ve ' gibi tüm geleneksel özel karakterler görünebilir. Yasak olan, ve bu sizi şaşırtabilir, -- karakter çiftidir. Bu dizinin kaçış işlemi belirtilmemiştir, bu yüzden onu nasıl değiştireceğiniz size kalmıştır. Aralarına boşluk koyabilirsiniz. Veya örneğin == ile değiştirebilirsiniz.

Soru No. 8

Sonuna yaklaşıyoruz, bu yüzden soruyu değiştirelim. Bu bağlamda değişkeni yazdırırken neye dikkat etmek gerektiğini düşünmeye çalışın:

<a href="<?= $str ?>">...</a>

Çözüm: Kaçış işlemine ek olarak, URL'nin javascript: gibi tehlikeli bir şema içermediğini doğrulamak da önemlidir, çünkü bu şekilde oluşturulan bir URL tıklandığında saldırganın kodunu çağırır.

Soru No. 9

Son olarak, gerçek uzmanlar için bir inci. Bu, modern bir JavaScript framework'ü, özellikle Vue kullanan bir uygulamanın örneğidir. Bakalım #app öğesi içinde değişkeni yazdırırken neye dikkat etmeniz gerektiğini düşünebilecek misiniz:

<div id="app">
    <?= $str ?>
</div>

<script src="http://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const app = new Vue({
    el: '#app',
    ...
})
</script>

Bu kod, #app öğesine oluşturulacak bir Vue uygulaması oluşturur. Vue, bu öğenin içeriğini şablonu olarak anlar. Ve şablon içinde, değişkenin yazdırılması veya javascript kodunun çağrılması anlamına gelen (örn. {{ foo }}) çift süslü parantezleri yorumlar.

Yani, #app öğesi içinde, < ve & karakterlerine ek olarak, {{ çiftinin de özel bir anlamı vardır, bunu Vue'nun kendi etiketi olarak yorumlamaması için başka bir karşılık gelen diziyle değiştirmemiz gerekir. Ancak HTML varlıklarıyla değiştirme bu durumda yardımcı olmaz. Nasıl başa çıkılır? Bir hile işe yarar: parantezlerin arasına boş bir HTML yorumu {<!-- -->{ ekleriz ve Vue böyle bir diziyi görmezden gelir.

Test Sonuçları

Testte nasıl performans gösterdiniz? Kaç doğru cevabınız var? En az 4 soruya doğru cevap verdiyseniz, en iyi %8'lik çözücüler arasındasınız – tebrikler!

Ancak, web sitenizin güvenliğini sağlamak için çıktıyı tüm durumlarda doğru bir şekilde işlemek zorunludur.

Sıradan bir HTML sayfasında ne kadar farklı bağlamın ortaya çıkabileceğine şaşırdıysanız, bilin ki hepsinden bahsetmedik bile. O zaman test çok daha uzun olurdu. Yine de, şablon sisteminiz bunu halledebiliyorsa, her bağlamda kaçış uzmanı olmanıza gerek yoktur.

Öyleyse onları deneyelim.

Şablon Sistemleri Nasıl Performans Gösteriyor?

Tüm modern şablon sistemleri, yazdırılan tüm değişkenleri otomatik olarak kaçıran otomatik kaçış (autoescaping) işleviyle övünür. Eğer bunu doğru yaparlarsa, web siteniz güvendedir. Eğer yanlış yaparlarsa, web sitesi tüm ciddi sonuçlarıyla birlikte XSS güvenlik açığı riskine maruz kalır.

Bu testin sorularından popüler şablon sistemlerini test edeceğiz, otomatik kaçışlarının ne kadar etkili olduğunu görmek için. PHP için şablon sistemlerinin dTest'i başlıyor.

Twig ❌

İlk sırada, en sık Symfony framework'ü ile birlikte kullanılan Twig (sürüm 3.5) şablon sistemi var. Ona tüm test sorularını cevaplama görevini vereceğiz. $str değişkeni her zaman hileli bir dize ile doldurulacak ve yazdırmasıyla nasıl başa çıktığına bakacağız. Sonuçları sağda görüyorsunuz. Cevaplarını ve davranışını oyun alanında keşfedin de inceleyebilirsiniz.

   {% set str = "<'\"&" %}
1) <p>{{ str }}</p>
2) <input value="{{ str }}">
3) <input value='{{ str }}'>

   {% set str = "foo onclick=evilCode()" %}
4) <input value={{ str }}>

   {% set str = "'\"\n\u{2028}" %}
5) <script>	let foo = '{{ str }}'; </script>
6) <p onclick="foo('{{ str }}')"></p>

   {% set str = "-- ---" %}
7) <!-- {{ str }} -->

   {% set str = "javascript:evilCode()" %}
8) <a href="{{ str }}">...</a>

   {% set str = "{{ foo }}" %}
9) <div id="app"> {{ str }} </div>

✅ <p>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script> let foo = &#039;&quot;u{2028}; </script>
❌ <p onclick="foo(&#039;&quot;u{2028})"></p>


❌ <!-- -- --- -->


❌ <a href="javascript:evilCode()">...</a>


❌ <div id="app"> {{ foo }} </div>

Twig dokuz testten altısında başarısız oldu!

Maalesef, Twig'in otomatik kaçış işlemi yalnızca HTML metninde ve niteliklerde çalışır ve o da yalnızca tırnak içine alınmışlarsa. Tırnak işaretleri eksik olduğunda, Twig herhangi bir hata bildirmez ve bir XSS güvenlik açığı oluşturur.

Bu özellikle can sıkıcıdır, çünkü React veya Svelte gibi popüler kütüphanelerde nitelik değerleri bu şekilde yazılır. Aynı anda Twig ve React kullanan bir programcı, bu nedenle tırnak işaretlerini tamamen doğal olarak unutabilir.

Twig'in otomatik kaçış işlemi diğer tüm örneklerde de başarısız olur. (5) ve (6) bağlamlarında, {{ str|escape('js') }} kullanarak manuel olarak kaçış yapmak gerekir, diğer bağlamlar için Twig bir kaçış fonksiyonu bile sunmaz. Hatalı bir bağlantının yazdırılmasına karşı koruma (8) veya Vue için şablon desteği (9) de yoktur.

Blade ❌❌

İkinci katılımcı, Laravel ve ekosistemiyle sıkı bir şekilde entegre olan Blade (sürüm 10.9) şablon sistemidir. Yeteneklerini yine test sorularımızda kontrol edeceğiz. Cevaplarını oyun alanında keşfedin de inceleyebilirsiniz.

   @php($str = "<'\"&")
1) <p>{{ $str }}</p>
2) <input value="{{ $str }}">
3) <input value='{{ $str }}'>

   @php($str = "foo onclick=evilCode()")
4) <input value={{ $str }}>

   @php($str = "'\"\n\u{2028}")
5) <script> let foo = {{ $str }}; </script>
6) <p onclick="foo({{ $str }})"></p>

   @php($str = "-- ---")
7) <!-- {{ $str }} -->

   @php($str = "javascript:evilCode()")
8) <a href="{{ $str }}">...</a>

   @php($str = "{{ foo }}")
9) <div id="app"> {{ $str }} </div>

✅ <p>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script>	let foo = &#039;&quot; ; </script>
❌ <p onclick="foo(&#039;&quot; )"></p>


❌ <!-- -- --- -->


❌ <a href="javascript:evilCode()">...</a>


❌❌ <div id="app"> &lt;?php echo e(foo); ?&gt; </div>

Blade dokuz testten altısında başarısız oldu!

Sonuç Twig'e benzer. Yine, otomatik kaçış yalnızca HTML metninde ve niteliklerde ve yalnızca tırnak içine alınmışlarsa çalışır. Blade'in otomatik kaçış işlemi diğer tüm örneklerde de başarısız olur. (5) ve (6) bağlamlarında, {{ Js::from($str) }} kullanarak manuel olarak kaçış yapmak gerekir. Diğer bağlamlar için Blade bir kaçış fonksiyonu bile sunmaz. Hatalı bir bağlantının yazdırılmasına karşı koruma (8) veya Vue için şablon desteği (9) de yoktur.

Ancak şaşırtıcı olan, Blade'deki @php direktifinin başarısız olmasıdır, bu da kendi PHP kodunun doğrudan çıktıya yazdırılmasına neden olur, bunu son satırda görüyorsunuz.

Smarty ❌❌❌

Şimdi PHP için en eski şablon sistemi olan Smarty (sürüm 4.3) test edeceğiz. Büyük bir sürprizle, bu sistemin aktif otomatik kaçış özelliği yoktur. Değişkenleri yazdırırken ya her seferinde {$var|escape} filtresini belirtmeniz ya da otomatik HTML kaçışını etkinleştirmeniz gerekir. Bununla ilgili bilgi belgelerde oldukça gizlidir.

   {$str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>

   {$str = "foo onclick=evilCode()"}
4) <input value={$str}>

   {$str = "'\"\n\u{2028}"}
5) <script>	let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>

   {$str = "-- ---"}
7) <!-- {$str} -->

   {$str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>

   {$str = "{{ foo }}"}
9) <div id="app"> {$str} </div>

✅ <p>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script> let foo = &#039;&quot;\u2028; </script>
❌ <p onclick="foo(&#039;&quot;\u2028)"></p>


❌ <!-- -- --- -->


❌ <a href="javascript:evilCode()">...</a>


❌ <div id="app"> {{ foo }} </div>

Smarty dokuz testten altısında başarısız oldu!

Sonuç ilk bakışta önceki kütüphanelere benzer. Smarty yalnızca HTML metninde ve niteliklerde, ve o da yalnızca değerler tırnak içine alınmışsa otomatik olarak kaçış yapabilir. Diğer her yerde başarısız olur. (5) ve (6) bağlamlarında, {$str|escape:javascript} kullanarak manuel olarak kaçış yapmak gerekir. Ancak bu yalnızca otomatik HTML kaçışı etkin değilse mümkündür, aksi takdirde bu kaçışlar birbiriyle çakışır. Smarty bu nedenle güvenlik açısından bu testin mutlak fiyaskosudur.

Latte ✅

Üçlüyü Latte (sürüm 3.0) şablon sistemi kapatıyor. Otomatik kaçışını deneyeceğiz. Cevaplarını ve davranışını oyun alanında keşfedin de inceleyebilirsiniz.

   {var $str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>

   {var $str = "foo onclick=evilCode()"}
4) <input value={$str}>

   {var $str = "'\"\n\u{2028}"}
5) <script>	let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>

   {var $str = "-- ---"}
7) <!-- {$str} -->

   {var $str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>

   {var $str = "{{ foo }}"}
9) <div id="app"> {$str} </div>

✅ <p>&lt;'"&amp;</p>
✅ <input value="&lt;&apos;&quot;&amp;">
✅ <input value='&lt;&apos;&quot;&amp;'>


✅ <input value="foo onclick=evilCode()">


✅ <script> let foo = "'\"\n\u2028"; </script>
✅ <p onclick="foo(&quot;&apos;\&quot;\n\u2028&quot;)"></p>


✅ <!--  - -  - - -  -->


✅ <a href="">...</a>


✅ <div id="app"> {<!-- -->{ foo }} </div>

Latte dokuz görevin hepsinde mükemmeldi!

HTML niteliklerindeki eksik tırnak işaretleriyle başa çıkmayı başardı, hem <script> öğesindeki hem de niteliklerdeki JavaScript'i işlemeyi başardı ve HTML yorumlarındaki yasak diziyle bile başa çıkabildi.

Dahası, bir saldırgan tarafından sahte bir bağlantıya tıklamanın kodunu çalıştırabileceği durumu önledi. Ve Vue için etiketlerin kaçışıyla başa çıkmayı başardı.

Bonus Test

Tüm şablon sistemlerinin temel yeteneklerinden biri bloklarla çalışmak ve bununla ilişkili şablon kalıtımıdır. Bu nedenle test edilen tüm şablon sistemlerine bir görev daha vereceğiz. Bir HTML niteliğinde yazdıracağımız bir description bloğu oluşturacağız. Gerçek dünyada, elbette blok tanımı alt şablonda ve yazdırması üst şablonda, yani örneğin layout'ta bulunurdu. Bu sadece basitleştirilmiş bir formdur, ancak blokları yazdırırken otomatik kaçışı test etmek için yeterlidir. Nasıl başardılar?

Twig: başarısız ❌ blokları yazdırırken karakterleri işlemez

{% block description %}
	rock n' roll
{% endblock %}

<meta name='description'
	content='{{ block('description') }}'>




<meta name='description'
	content=' rock n' roll '> ❌

Blade: başarısız ❌ blokları yazdırırken karakterleri işlemez

@section('description')
	rock n' roll
@endsection

<meta name='description'
	content='@yield('description')'>




<meta name='description'
	content=' rock n' roll '> ❌

Latte: başarılı ✅ blokları yazdırırken sorunlu karakterleri doğru bir şekilde işledi

{block description}
	rock n' roll
{/block}

<meta name='description'
	content='{include description}'>




<meta name='description'
	content=' rock n&apos; roll '> ✅

Neden Bu Kadar Çok Web Sitesi Savunmasız?

Twig, Blade veya Smarty gibi sistemlerdeki otomatik kaçış, basitçe beş karakter <>"'&'yi HTML varlıklarıyla değiştirerek çalışır ve bağlamı hiçbir şekilde ayırt etmez. Bu nedenle yalnızca bazı durumlarda çalışır ve diğer tüm durumlarda başarısız olur. Naif otomatik kaçış tehlikeli bir özelliktir, çünkü yanlış bir güvenlik hissi yaratır.

Bu nedenle, günümüzde web sitelerinin %27'sinden fazlasının kritik güvenlik açıklarına, özellikle de XSS'e sahip olması şaşırtıcı değildir (kaynak: Acunetix Web Vulnerability Report). Bundan nasıl çıkılır? Bağlamları ayırt eden bir şablon sistemi kullanın.

Latte, PHP'deki tek şablon sistemidir ki şablonu yalnızca bir karakter dizisi olarak görmez, HTML'yi anlar. Etiketlerin, niteliklerin vb. ne olduğunu anlar. Bağlamları ayırt eder. Ve bu nedenle HTML metninde doğru şekilde kaçış yapar, HTML etiketi içinde farklı, JavaScript içinde farklı vb.

Latte bu nedenle tek güvenli şablon sistemini temsil eder.


Ayrıca, HTML anlayışı sayesinde, kullanıcıların sevdiği harika n:attributes özelliğini sunar:

<ul n:if="$menu">
	<li n:foreach="$menu->getItems() as $item">{$item->title}</li>
</ul>