CVE-2026-4378 — Stored XSS — shaggy

Türkiye’de yaygın kullanılan bir e-ticaret altyapısında, profil güncelleme endpoint’indeki adi parametresi üzerinden kalıcı (stored) XSS tetiklenebiliyor. Payload hem mağaza arayüzünde hem de yönetim panelindeki üye listesinde render ediliyor, yani müşteri hesabından mağaza yöneticisinin oturumuna ulaşıyor.

Üreticinin adını yazıda geçirmiyorum, resmi referanslar aşağıda duruyor. Ekran görüntülerini de olduğu gibi bırakmadım, üçüncü kişilere ait e-posta ve telefon bilgilerini maskeledim.

Zafiyet: CWE-79 Stored XSS · Sürüm: 4.5.001 · CVE: CVE-2026-4378

USOM üzerinden koordineli bildirim yapıldı. 18 Mart 2026’da CVE tahsis edildi, 28 Ağustos 2026’da TR-26-0944 bildirimiyle ilan edildi. Yazının yayımlandığı tarihte zafiyetin giderilip giderilmediği doğrulanamamıştır.


Ortam

Ekim 2025. Firmanın sitesindeki demo talebi formunu doldurdum, bana bir hesap açtılar. Verilen erişim hem mağaza tarafındaki bir müşteri hesabını hem de yönetim panelini kapsıyordu.

Sunucu tarafı Server: Microsoft-IIS/10.0 ve X-Powered-By: ASP.NET dönüyor, endpoint’ler .asp uzantılı. Yani klasik ASP, ASP.NET değil. Yönetim panelinin sol alt köşesinde sürüm yazıyordu: 4.5.001. Ekran görüntüsünü aldım.

Oturum açtıktan sonra çerez kavanozu şöyleydi (kısaltılmış):

1indirim=0; lang=1; at=87260779dba97bdb6554cd7ca39f6cb614; notification=0;
2customerlastname=KURTMAN; customername=KORAY; kad=demo%40[redacted]%2Ecom;
3idY=6; statusY=1; HttpOnly; ASPSESSIONIDQGBSRRRA=PKEHGPBDDNHAEHCBJJDAOFOL;
4usertype=user; uyeliksizIslem=; uname=test+test; kid=470756;
5username=testtest%40hotmail%2Ecom; KutuGorunumu=katalog; posetistemiyorum=1;
6chkohash=dGVtcHx8dGVtcHx8fHx8fHx8...; gilce=2000746000; gil=ADIYAMAN;
7ccek=True; ccek2=True; sipno=AT%2D000000100653; sipbildirim=0

Burada dikkatimi çeken üç şey oldu:

usertype=user istemci tarafında tutulan bir rol göstergesi. Sunucu kararlarını buna dayandırıyorsa yetki yükseltme yüzeyi olur. Bunu admin benzeri bir değerle denemedim, demo ortamında yetki yükseltmeye çalışmak istemedim.

HttpOnly diye bir çerez var. İsmi bu. Değeri yok. Bunun nereden geldiği yanıt başlıklarında görünüyor, birazdan geleceğim.

chkohash base64 görünümlü ve çözüldüğünde temp||temp||...||350||1901011010 1||1|| gibi boru işaretiyle ayrılmış bir yapı çıkıyor. İmzasız görünen serileştirilmiş bir sepet/oturum verisi. Buna da dokunmadım.

Nereye baktım

Bu tür ürünlerde kullanıcının serbest metin yazabildiği alanlara bakıyorum, çünkü o veri hem müşteri arayüzünde hem yönetim panelinde render ediliyor ve iki taraf çoğu zaman ayrı şablonlarla yazılmış oluyor. Bir tarafta kodlama yapılmış olması diğerini bağlamıyor.

Gezdiğim yerler: üyelik bilgileri, adres defteri, sipariş notu, sipariş geçmişi, ürün yorumu. Payload’ı ilk olarak üyelik bilgileri sayfasındaki isim alanına koydum, çünkü isim mağaza arayüzünün üst barında sürekli görünüyor ve yönetim panelinde üye listelerine düşme ihtimali yüksek.

adi parametresi

Kaydet dediğimde giden istek /usrinf.asp adresine POST atıyor ve isim alanı adi parametresiyle taşınıyor. Alana şunu yazdım:

1<img src=x onmouseover=prompt(1)>

<script> yerine <img> seçmemin sebebi, verinin innerHTML üzerinden basıldığı durumlarda <script> etiketinin zaten çalışmaması ve bunun yanlış negatif üretmesi. onerror yerine onmouseover seçtim çünkü paylaşılan bir demo ortamındaydım, orada başka demo kullanıcıları ve firma çalışanları da var. Kendiliğinden ateşleyen kalıcı bir payload bırakmak istemedim. alert yerine prompt, aynı ortamda benden önce dolaşmış birinin izinden ayırt edebilmek için.

POST /usrinf.asp HTTP/2
Host: demo.[redacted].com
Origin: https://demo.[redacted].com
Referer: https://demo.[redacted].com/usrinf.asp
Content-Type: application/x-www-form-urlencoded
Content-Length: 105

adi=%3Cimg+src%3Dx+onmouseover%3Dprompt%281%29%3E&soyadi=test&tel=4545455555&action=update&sifre=&sifre2=

Formun profil güncellemesiyle birlikte sifre ve sifre2 alanlarını da boş gönderdiğine dikkat edin. Mevcut parolayı soran bir alan yok. Bunu ayrı bir başlık altında incelemedim ama bir CSRF veya oturum devralma senaryosunda parola değişikliğinin aynı istekle yapılabiliyor olması bakılmaya değer.

Yanıt:

HTTP/2 200 OK
Server: Microsoft-IIS/10.0
X-Powered-By: ASP.NET
Set-Cookie: uname=%3Cimg+src%3Dx+onmouseover%3Dprompt%281%29%3E+test; path=/
Set-Cookie: HttpOnly; Secure
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
Content-Length: 73200

Manipüle edilen adi parametresinin request ve response’u

200 OK. Kayıt kabul edildi, filtreleme veya uzunluk kısıtı görünmüyor. Aynı formdaki soyadi ve tel alanlarını denemedim, büyük ihtimalle aynı davranıyorlar ama denemediğim için iddia edemem.

Yanıtta göze çarpan diğer şeyler

İkinci Set-Cookie satırı bozuk. Set-Cookie: HttpOnly; Secure geçerli bir çerez tanımı değil, isim/değer çifti yok. Tarayıcı bunu HttpOnly adında değersiz bir çerez olarak saklıyor, request’teki kavanozda görünmesinin sebebi bu. Muhtemelen bayrakları eklemeye çalışan bir kod parçası çıktıyı yanlış üretiyor. Asıl sonucu şu: bayrakları eklemesi beklenen kod, bir önceki satırdaki uname çerezine bunları eklemiyor.

uname çerezine payload yazılıyor ve HttpOnly bayrağı yok. Çerez path=/ ile set edilmiş. Bu haliyle aynı origin’de çalışan JavaScript tarafından okunabilir. Bunu ayrı bir bulgu olarak bildirmedim, XSS ile birlikte etkiyi büyüten bir ayrıntı olarak not ediyorum.

Güvenlik başlıklarında tek eksik CSP. HSTS, X-Frame-Options, nosniff, Referrer-Policy ve Permissions-Policy var. Content-Security-Policy yok. Buradan uygulamanın geliştirme kültürü hakkında kesin bir sonuç çıkarmıyorum, ama bir gözlem olarak şunu söyleyebilirim: bu beş başlık sunucu katmanında koda dokunmadan eklenebilir, çıktı kodlaması ve kullanılabilir bir CSP ise şablonların içine girmeyi gerektirir. Bu ayrımın, nereye bakacağıma karar verirken işime yarayan bir sinyal olduğunu düşünüyorum.

Sayfa başlığında bozuk karakter var: <title> içinde à yelik Bilgilerim yazıyor, Üyelik Bilgilerim olması gerekiyor. <meta charset="UTF-8"> tanımlı olmasına rağmen içerik başka bir kodlamayla üretilmiş. Charset karışıklıkları bazen filtre atlatmada işe yarar, burada buna ihtiyaç olmadı.

Meta etiketlerinde başka bir mağazadan kalma değerler var. og:site_name, og:title ve og:description alanları bu mağazaya değil, aynı altyapıyı kullanan farklı bir siteye ait. Şablonların kiracılar arasında tam ayrışmadığını gösteriyor. Bunun peşine düşmedim.

CSS //satis.[redacted].com/v3-cdn/... üzerinden, protokolsüz bir yoldan çekiliyor.

İkinci render noktası

Kaydettikten sonra mağaza arayüzüne döndüm. Üst bardaki hesap menüsünde ismimin olması gereken yerde kırık görsel ikonu duruyordu, üzerine gelince payload çalıştı:

Kullanıcının tarayıcısında çalışan JavaScript

Bu haliyle self-XSS. Kendi hesabım, kendi tarayıcım. Raporlanacak bir şey yok, çünkü kimseyi etkilemiyor. Payload’ın yazıldığı yer tek, okunduğu yerler ise farklı olabilir, o yüzden aynı verinin göründüğü diğer ekranları gezmeye devam ettim.

Yönetim panelinde Üye menüsü altındaki üye listesine girdiğimde (/yonetimV2/3100) payload orada da çalıştı:

Yönetim panelinde üye listesi, payload çalışıyor

Listede benim satırım isim sütununda kırık görsel olarak render ediliyor. Yönetici imleci o satırın üzerinden geçirdiğinde JavaScript onun oturumunda çalışıyor. Payload sunucuda saklı olduğu için kurbanın bir bağlantıya tıklaması gerekmiyor, kendi rutin işini yaparken tetikleniyor. Ve müşteri hesabından yönetici oturumuna geçiyor. prompt(1) yerine oturum verisini dışarı gönderen bir satır olsaydı sonuç mağaza panelinde oturum devralma olurdu.

Doğrulamayla ilgili bir kısıt var, raporda da bunu böyle yazdım: kullandığım demo hesabı bana yönetim paneline de erişim verdiği için ikinci render noktasını kendim görebildim. Gerçek bir kurulumda o paneli başka biri açar. Etki aynı kalır ama benim doğrudan gözlemim yerine çıkarım olurdu.

Buraya kadar uygulama bana XSS dışında birkaç tane “buna sonra bakarım” bıraktı: chkohash, boş giden parola alanları, bir de sipariş notu, fatura çıktısı ve CSV export gibi gezemediğim render noktaları. Sonra ortam kapandı ve hiçbirine bakamadım. Sürüm ekran görüntüsünü ilk gün almış olmam şans eseri iyi oldu, başvurunun devamı ona dayandı.

Bildirim süreci

Ekim 2025. Bulguyu ekran görüntüleriyle USOM’a ilettim. Gelen cevap, zafiyetin demo ortamında tespit edildiği gerekçesiyle işlem yapılmayacağı yönündeydi:

“Başvuruya konu yazılımların ön sürümlerinde veya test (demo) ortamlarında bulunan zafiyetlere işlem yapılmaz.”

Politikanın kendisi savunulabilir. Bir firmanın test sunucusuna attığı yarım kalmış koda CVE vermek kimseyi korumaz.

Mart 2026. İtiraz ederken politikayı değil, politikanın uygulandığı olguyu tartıştım. Firmanın kendi sitesindeki yazılım güncellemeleri sayfasında yayımlanan en güncel sürüm 4.4.004 olarak duruyordu. Zafiyeti bulduğum demo ortamı ise 4.5.001 çalıştırıyordu, bunun ekran görüntüsü elimdeydi.

Yani demo, üreticinin müşterilerine dağıttığı sürümden daha yeniydi. Ön sürüm değil, bir sonraki güncellemede müşterilere inecek koddu. Zafiyet de ortama özgü bir yapılandırmadan değil, adi parametresinin çıktısının kodlanmamasından geliyordu. İkinci e-postamda sürüm geçmişi bağlantısını verdim ve iki numarayı yan yana koydum.

18 Mart 2026. Aynı gün CVE tahsis edildi:

“CWE-79 … zafiyetine ilişkin CVE-2026-4378 kodu tahsis edilmiştir. Zafiyetin giderilmesini müteakip CVE içeriği doldurulacak ve ilan edilecektir.”

Retest. CVE’nin ilan edilmesi için zafiyetin kapatılması gerekiyordu. Tekrar test etmek için ortama girmeye çalıştığımda demo kapatılmıştı. Zafiyetin bugün kapalı olup olmadığını bilmiyorum, doğrulayamadım. Elimde Ekim 2025’te alınmış ekran görüntülerinden başka bir şey yok.

28 Ağustos 2026. CVE ilan edildi.

Bildirimdeki “Çözüm” alanı

Bildirimin Çözüm bölümü

“Siber Güvenlik Başkanlığı, üretici firma tarafından ilgili zafiyetin giderilmemesinden dolayı ürünü kullananlara muadil başka bir uygulama kullanmalarını tavsiye etmektedir.”

Bir güvenlik bildiriminin çözüm alanında normalde bir sürüm numarası veya bir yapılandırma değişikliği olur. Burada ürünü bırakıp başkasına geçme tavsiyesi var. Teknik olarak söylenecek çok bir şey yok: XSS bildirildi, otorite on ay takip etti, yama çıkmadı, geriye kalan tek tavsiye bu oldu. Bir isim alanındaki otuz karakterin görebileceği en pahalı workaround olabilir.

Ürünü kullanan mağazalar açısından pratik sonuç şu: yama yok, ama zafiyetin varlığı artık kamuya açık ve platform değiştirmek küçük bir mağaza için veri taşıma ve entegrasyon maliyeti demek. Bunun nasıl düzeltileceğine dair elimde net bir öneri yok. Söyleyebileceğim tek şey, burada eksik olanın kod kalitesi olmadığı. XSS her ülkedeki her büyüklükteki firmanın kodundan çıkıyor. Bu vakada ayrışan nokta, üreticinin hiç yanıt vermemesinin kimseye bir maliyeti olmaması.

Düzeltme

Zafiyetin kaynağı çıktı kodlamasının olmaması. Klasik ASP tarafında:

<%= Server.HTMLEncode(rs("adi")) %>

Kodlama girdi tarafında değil çıktı tarafında olmalı, çünkü aynı veri en az iki ayrı şablonda basılıyor ve kayıt sırasında temizlemek veritabanında zaten duran satırları çözmüyor.

Yanına:

  • Content-Security-Policy. Uygulamanın JavaScript’i nasıl yüklediğine bağlı olarak, inline event handler’ları kısıtlayan bir politika bu payload’ın çalışmasını engelleyebilir. Mevcut kod tabanında inline script kullanımı yaygınsa böyle bir politikayı devreye almak tek satırlık bir iş olmaz, aşamalı gitmek gerekir.
  • Yönetim paneli listeleri. Kullanıcı verisini gösteren yönetici ekranları, müşteri arayüzüyle aynı kodlama disiplinine tabi olmalı.
  • Çerez bayrakları. Bozuk Set-Cookie: HttpOnly; Secure satırı düzeltilmeli ve bayraklar uname gibi çerezlerin kendi tanımına eklenmeli.

Kapanış

Teknik tarafta yeni bir şey yok, kullanıcı girdisi iki ayrı arayüzde kodlanmadan basılıyor. Benim açımdan eksik kalan şey bulgunun kapandığını hiç görememem, o yüzden rapor hâlâ yarım duruyor.

Bir dahaki sefere aynı yerden başlayacağım. Bir demo talebi, bir isim alanı, otuz karakter. O kısıma gelene kadar hoşçakalın. :)