Yazılar · Bilişim hukuku

Bir yorumla siteye nasıl zararlı kod eklenebildi? Fast Velocity Minify açığı (CVE-2026-48201)

· Av. Alpaslan Fatih Sözer, Denizli Barosu

Kısaca. Fast Velocity Minify eklentisinin 3.5.7 ve önceki sürümlerinde, oturum açmamış biri bıraktığı bir yorumla sayfaya, o sayfayı açan ziyaretçilerin tarayıcısında çalışan betik ekleyebiliyordu.

  • Açık CVE-2026-48201 numarasıyla yayımlandı (CVSS 7.1, kalıcı XSS). 28 Ekim 2026'da çıkan 3.5.8 sürümünde kapatıldı.
  • Kök neden, eklentinin sayfayı işlerken kullandığı HTML ayrıştırıcısının yer tutucularıydı: biçimleri tahmin edilebiliyordu ve bir öznitelik değerinin içinde bile kaçışsız geri yükleniyorlardı.
  • Eklentiyi kullanıyorsanız 3.5.8 ya da sonraki bir sürüme güncelleyin ve sayfa önbelleğinizi temizleyin.

Fast Velocity Minify, WordPress sitelerinin CSS, JavaScript ve HTML çıktısını küçülterek sayfaları hızlandıran bir eklenti ve yaklaşık 30 bin sitede kurulu. Eklenti, sitenin ürettiği her sayfayı ziyaretçiye gitmeden önce bir tampona alıyor, içine gömülü bir HTML ayrıştırıcısıyla parçalarına ayırıyor, gereksiz boşlukları ve yorumları temizleyip yeniden birleştiriyor. Aynı ayrıştırıcı kütüphane başka birçok eklentinin içinde de geliyor ve bazılarında aynı sınıftan hatalar daha önce düzeltilmişti. Ekim 2026'da bu kütüphaneyi kullanan eklentileri kendi bilgisayarımdaki deneme kurulumunda incelerken Fast Velocity Minify'ın güncel sürümünün hala açık olduğunu gördüm ve Patchstack üzerinden bildirdim.

Ayrıştırıcı sayfayı nasıl işliyordu?

Ayrıştırıcı, belgeyi okumadan önce içeriğine karışmak istemediği parçaları kenara koyuyor: <script> ve <style> gövdeleri, HTML yorumları ve <code> etiketlerinin içi. Her birinin yerine bir yer tutucu yazıyor, belgeyi işledikten sonra yer tutucuları asıl içerikle değiştiriyor. 3.5.7'de yer tutucu şöyle üretiliyordu:

// libs/simplehtmldom/simple_html_dom.php (3.5.7)
$key = '___noise___' . sprintf('% 5d', count($this->noise) + 1000);

Yani ilk kenara konan parça ___noise___ 1000, sonraki ___noise___ 1001 diye gidiyordu. Geri yükleme ise metinde bu biçimde bir dize gördüğü her yerde yapılıyordu, öznitelik değerleri dahil:

// libs/simplehtmldom/simple_html_dom.php (3.5.7), parse_attr()
$value = $this->restore_noise($value);

Bu iki satır birlikte iki kusur doğuruyordu. Birincisi, yer tutucunun biçimi sabit ve numarası sıralı olduğu için sayfaya yazı yazabilen herkes bir yer tutucuyu taklit edebiliyordu. İkincisi, öznitelik değerinin içine geri yüklenen içerik kaçışsız yazılıyordu: kenara konmuş bir parçanın içinde tırnak işareti varsa, bu tırnak özniteliği kapatıyor ve arkasından gelen metin yeni öznitelikler olarak okunuyordu.

Bir yorum bunu nasıl tetikliyordu?

WordPress yorumlarda <abbr title="..."> ve <code> gibi birkaç zararsız etikete izin verir ve yorumu kaydederken içindeki tehlikeli kısımları temizler. Sorun, temizlenmiş ve zararsız haliyle kaydedilen yorumun sayfaya çıkarken eklentinin ayrıştırıcısından geçmesiydi. Yorum, title değeri olarak bir yer tutucuyu taklit eden bir metin ve hemen arkasında tırnak işaretiyle başlayan bir <code> gövdesi içeriyorsa, ayrıştırıcı o gövdeyi kenara koyuyor, sonra title değerinde kendi yer tutucusunu gördüğünü sanıp gövdeyi oraya geri yüklüyordu. Zararsız bir örnekle:

<!-- veritabanında kayıtlı yorum -->
<abbr title="___noise___ 1038">...</abbr><code>" data-ornek="</code>

<!-- eklentinin ziyaretçiye gönderdiği sayfa -->
<abbr title="" data-ornek="">...</abbr>

Veritabanındaki yorum zararsızdı ve eklenti kapatıldığında sayfada da zararsız görünüyordu. Yeni öznitelik, yalnız eklentinin sayfayı yeniden yazdığı anda ortaya çıkıyordu. Örnekteki data-ornek yerine bir olay işleyicisi yazıldığında ise sayfayı açan her ziyaretçinin tarayıcısında, hiçbir şeye tıklamadan, sitenin adına bir betik çalışıyordu. Deneme kurulumumda bunu yalnız alan adını ekrana yazdıran bir uyarı kutusuyla ölçtüm.

Doğru numarayı bulmak için tahmin de gerekmiyordu. Eklenti, adresinde sorgu dizesi (? ile başlayan bir ek) bulunan sayfaları hiç işlemiyor. Aynı yazıyı adresin sonuna bir sorgu ekleyerek açan biri, ayrıştırıcının göreceği belgenin aynısını görüyor ve kenara konacak parçaları sayarak numarayı hesaplayabiliyordu. Yanlış numara da zararsız bir ıska değildi: kütüphanenin geri yükleme döngüsü tabloda olmayan bir yer tutucuyla karşılaşınca sonsuz döngüye giriyor ve sayfa yanıt vermez hale geliyordu. Kütüphanenin kendi kodunda bu tehlikeyi anlatan bir “yapılacak” notu zaten vardı.

Kimler etkileniyordu?

  • Saldırgan tarafında: hesap gerekmiyordu. WordPress yeni bir yorumcunun ilk yorumunu varsayılan olarak onaya düşürdüğü için saldırgan önce sıradan bir yorum bırakıyor, yönetici bunu onayladıktan sonra aynı adresten gelen yorumlar doğrudan yayımlanıyordu. Bu tek adım saldırganın elinde değildi ama sitenin olağan işleyişiydi.
  • Ziyaretçi tarafında: varsayılan ayarlarda eklenti oturum açmış kullanıcılar için sayfayı işlemiyor. Betik bu yüzden oturum açmamış ziyaretçilerin tarayıcısında çalışıyordu; yöneticinin oturumunu ele geçirmeye dair bir iddiada bulunmuyorum.
  • Site tarafında: eklentiyi kurup etkinleştirmek yetiyordu, hiçbir ayara dokunmak gerekmiyordu. Kalıcı bağlantı yapısı /?p=1 biçiminde kalmış siteler etkilenmiyordu, çünkü eklenti sorgu dizeli adresleri işlemiyor.

Patchstack açığı 7.1 puanla değerlendirdi. Test yalnız kendi bilgisayarımdaki, dışarıya bağlantısı kapalı kurulumda yapıldı; başkasına ait hiçbir siteye istek gönderilmedi.

Üretici açığı nasıl kapattı?

3.5.8'de iki kusur da ayrı ayrı ele alındı. Yer tutucu artık her ayrıştırmada rastgele üretilen bir önek taşıyor, bu yüzden sayfaya yazı yazan biri onu taklit edemiyor:

// libs/simplehtmldom/simple_html_dom.php (3.5.8)
try { $rand = bin2hex(random_bytes(8)); } catch (Throwable $e) { $rand = md5(uniqid((string) wp_rand(), true)); }
$this->noise_marker = '___noise_' . $rand . '___';

İkincisi, ayrıştırıldıktan sonra değeri değişen öznitelikler artık yazılırken kaçışlanıyor, böylece içlerindeki bir tırnak özniteliği kapatamıyor:

// libs/simplehtmldom/simple_html_dom.php (3.5.8), makeup()
if (!array_key_exists($key, $this->attr_parsed) || $this->attr_parsed[$key] !== $val) {
    $val = (string) $val;
    ...
    if ($quote === '"') { $val = str_replace('"', '&quot;', $val); }
    elseif ($quote === '\'') { $val = str_replace('\'', '&#039;', $val); }
}

Geri yükleme döngüsü de yeniden yazıldı: tanımadığı bir yer tutucuyla karşılaştığında onu silip devam ediyor, böylece sayfanın kilitlenmesine yol açan sonsuz döngü de kapandı. Kenara konacak parçalar artık düzenli ifadelerle değil, belgenin okunduğu sırayla bulunuyor. Sürüm notunda açığın Patchstack üzerinden tarafımdan bildirildiği de yazıyor.

Geliştiriciler için ders ne?

  • İçeriği geçici olarak kenara koyan her mekanizmada yer tutucu, girdinin tahmin edemeyeceği bir değer olmalı. Sabit bir önek ve sıralı bir numara, girdinin mekanizmayı taklit etmesine kapı açar.
  • Bir değeri geri yazarken nereye yazdığınıza göre kaçışlayın. Metin düğümüne güvenle yazılabilen bir içerik, tırnak içindeki bir öznitelik değerinde yeni öznitelik açabilir.
  • Eklentinize gömdüğünüz üçüncü taraf kütüphanelerdeki “yapılacak” ve “bilinen sorun” notlarını kendi kodunuzun sorunu sayın. Bu eklentide sayfayı kilitleyen döngünün tehlikesi, kütüphanenin kendi yorumunda yazılıydı.

Site sahibi ne yapmalı?

  • Eklentiyi 3.5.8 ya da sonraki bir sürüme güncelleyin.
  • Güncellemeden sonra kullandığınız sayfa önbelleğini (barındırma firmasının önbelleği dahil) temizleyin. Eklentinin eski sürümüyle üretilmiş sayfalar önbellekte kalmışsa, sorunlu çıktı güncellemeden sonra da bir süre sunulabilir.
  • Yorumlarınızda ___noise___ ile başlayan bir ifade geçiyorsa o yorumu inceleyin. Bu ifadenin sıradan bir yorumda bulunması için bir sebep yoktur.
  • Hız eklentileri sitenin bütün çıktısını yeniden yazar. Bu türden eklentilerin güncellemelerini, sitenin geri kalanından daha yakından takip edin.

Bunun hukuki bir boyutu var mı?

Bu açık, sitenin veritabanındaki kişisel verileri doğrudan açığa çıkarmıyordu. Ama ziyaretçinin tarayıcısında sitenin adına çalışan bir betik, sayfaya sahte bir form ekleyebilir, ziyaretçinin o sayfaya girdiği bilgileri başka bir adrese gönderebilir ya da ziyaretçiyi başka bir siteye yönlendirebilir. Site sahibi, sitesinde topladığı iletişim formu, üyelik ya da sipariş bilgileri bakımından veri sorumlusudur ve kişisel verilere hukuka aykırı olarak erişilmesini önlemek için uygun güvenlik düzeyini sağlayan teknik ve idari tedbirleri almak zorundadır (KVKK m. 12/1). Üreticinin yayımladığı bir güvenlik yamasını zamanında kurmak bu tedbirlerin en temel olanıdır. Kişisel veriler bir saldırı sonucunda başkalarının eline geçmişse veri sorumlusu bunu en kısa sürede ilgili kişilere ve Kurula bildirmek zorundadır (KVKK m. 12/5); bu yüzden yamayı geç kuran bir site sahibi, güncellemeden önceki dönemde sitesinde olağan dışı bir yorum ya da yönlendirme görmüşse durumu bu gözle de değerlendirmelidir.

Zaman çizelgesi

  • 22 Ekim 2026: Açık deneme kurulumunda doğrulandı ve Patchstack üzerinden bildirildi. Patchstack bulguyu aynı gün doğruladı ve üreticiye iletti.
  • 28 Ekim 2026: Fast Velocity Minify 3.5.8 yayımlandı.
  • 30 Ekim 2026: CVE-2026-48201 atandı ve Patchstack kaydı yayımlandı.

Değerlendirmem

Bu açıkta saldırganın yaptığı şey, WordPress'in yorumlarda zaten izin verdiği iki etiketi belirli bir sırayla kullanmaktı. WordPress yorumu doğru temizlemişti; açık, temizlenmiş içeriği sayfaya çıkarken yeniden yazan bir hız eklentisinin içinde doğuyordu. Bir siteye eklenen her katman, kendinden önceki katmanların aldığı önlemleri bozabilir. Üreticinin düzeltmesi iyi bir örnek, çünkü önerdiğim tek satırlık geçici çözümle yetinmeyip yer tutucuyu tahmin edilemez hale getirdi, kaçışı ekledi ve aynı koddaki kilitlenme sorununu da birlikte kapattı.

Kaynaklar

Bu yazı 30 Ekim 2026 itibarıyla durumu anlatıyor.

Süreç, süreler ve sık sorulanlar: Bilişim hukuku, siber güvenlik ve KVKK sayfası