Yazılar · Bilişim hukuku

İki eşittir işareti bir mağazanın ödeme anahtarını nasıl değiştirtti? Swish for WooCommerce açığı (CVE-2026-111835)

· Av. Alpaslan Fatih Sözer, Denizli Barosu

Kısaca. BjornTech'in Swish for WooCommerce eklentisinin 3.7.8 ve önceki sürümlerinde, oturum açmamış biri tek bir istekle mağazanın ödeme servisine bağlanırken kullandığı kimlik bilgisini kendi gönderdiği değerle değiştirebiliyordu.

  • Açık CVE-2026-111835 numarasıyla yayımlandı (CVSS 6.5, yetki kontrolü eksikliği). 18 Ekim 2026'da çıkan 3.7.9 sürümünde kapatıldı.
  • Kök neden PHP'nin gevşek karşılaştırmasıydı: true değeri boş olmayan her metne “eşit” sayıldığı için, gizli değerleri bilmeden iki kontrol de geçilebiliyordu.
  • Eklentiyi kullanıyorsanız 3.7.9 ya da sonraki bir sürüme güncelleyin ve Swish bağlantınızı yeniden kurun.

Swish, İsveç'te yaygın kullanılan bir mobil ödeme yöntemi; BjornTech'in eklentisi WooCommerce mağazalarının bu yöntemle ödeme almasını sağlıyor. Mağaza, BjornTech'in aracı servisine bağlanırken bir yenileme jetonu (refresh token) alıyor ve eklenti sonraki her ödeme işleminde servise kendini bu jetonla tanıtıyor. Ekim 2026'da eklentinin bu bağlantı akışını kendi bilgisayarımdaki deneme kurulumunda incelerken, jetonun yazıldığı ucun kimden geldiğini gerçekte denetlemediğini gördüm ve Patchstack üzerinden bildirdim. Bu yazıda kök nedeni, ki PHP'de çok sık görülen bir hatadır, kod düzeyinde anlatıyorum.

Açık tam olarak neydi?

Eklenti, WooCommerce'in eski API altyapısı üzerinden /?wc-api=swish_admin adresinde bir uç açıyor. Bu uç, bağlantı kurulurken BjornTech servisinin mağazaya geri çağrı yapması için var ve gelen JSON gövdesindeki yenileme jetonunu mağazanın ayarlarına kaydediyor. WooCommerce bu tür uçları herkese açık çalıştırır; çağıranın gerçekten BjornTech servisi olup olmadığını denetlemek tamamen eklentiye kalır.

Eklentinin denetimi iki karşılaştırmadan ibaretti: gelen nonce değeri, bağlantı başlarken saklanan tek kullanımlık değerle; gelen uuid değeri de mağazanın hesap kimliğiyle karşılaştırılıyordu. İkisi de tutarsa jeton kaydediliyordu.

// classes/woo-swish-payment-gateway.php (3.7.8)
$body  = json_decode( file_get_contents( "php://input" ) );
$nonce = get_site_transient( 'handle_swish_account' );   // bağlantı akışı yoksa false

if ( $body->nonce == $nonce ) {
    if ( $body->uuid == $this->account_uuid ) {
        $this->refresh_token = $body->refresh_token;
        $this->update_option( 'swish_refresh_token', $this->refresh_token );
        delete_site_transient( 'swish_access_token' );
        ...

Neden iki eşittir işareti yetmiyor?

PHP'de == gevşek karşılaştırmadır: iki değerin türü farklıysa önce birini diğerinin türüne çevirir, sonra karşılaştırır. Bir metin ile mantıksal bir değer karşılaştırıldığında metin mantıksal değere çevrilir ve boş olmayan her metin true olur. Gövde JSON olarak okunduğu için gönderen kişi değerin türünü de seçebiliyordu. Bu iki kusur birleşince ortaya iki ayrı yol çıkıyordu.

Bağlantı akışı açık değilken. Tek kullanımlık değer yalnız yönetici bağlantı ekranındayken saklanıyor, akış bitince siliniyordu. Geri kalan bütün zamanlarda get_site_transient() false döndürüyordu. Gövdede nonce hiç gönderilmezse $body->nonce null oluyor ve null == false doğru sayılıyordu. Mağaza henüz bağlanmamışsa hesap kimliği de boş metindi ve aynı yolla geçiliyordu.

Mağaza bağlıyken. Bu durumda saklanan değerler gerçek, rastgele metinlerdi. Ama gönderen kişi değerleri metin olarak değil mantıksal değer olarak yollayabiliyordu:

var_dump( true == "9f8e7d6c5b" );                             // bool(true)
var_dump( true == "a1b2c3d4-e5f6-7890-abcd-ef1234567890" );   // bool(true)

Yani {"nonce": true, "uuid": true, "refresh_token": "..."} biçimindeki bir gövde, gizli değerlerin hiçbirini bilmeden iki kontrolü de geçiyordu. İkinci yol önemliydi, çünkü açığı yalnız kurulmamış mağazalara değil, çalışan ve bağlı mağazalara karşı da kullanılabilir kılıyordu. Bunu raporda ayrıca vurguladım: yalnız tek kullanımlık değerin hep dolu olmasını sağlayan bir düzeltme birinci yolu kapatır, ikinciyi açık bırakırdı.

Sonucu neydi?

Başarılı bir istek, mağazanın kendi yenileme jetonunu silip yerine gönderilen değeri yazıyor, önbellekteki geçerli erişim jetonunu da siliyordu. Böylece bir sonraki ödeme işleminde eklenti, servise mağazanın değil gönderenin verdiği jetonla bağlanmaya çalışıyordu. Ayarlar ekranı ise bağlantıyı “kurulu” göstermeye devam ediyordu, çünkü o ekran yalnız jeton alanının dolu olup olmadığına bakıyordu.

Ölçtüğüm şeyin sınırını da belirtmeliyim: deneme kurulumumda dışarıya giden bağlantılar kapalıydı, bu yüzden BjornTech servisinin bu jetonla ne cevap vereceğini denemedim ve ödemelerin nereye gideceğine dair bir iddiada bulunmuyorum. Ölçülen, mağazanın kendi kimlik bilgisinin yerinde olmadığı ve bir yabancının belirlediği değerin onun yerini aldığıdır. Patchstack açığı 6.5 puanla değerlendirdi. Test yalnız kendi bilgisayarımdaki kurulumda yapıldı; başkasına ait hiçbir mağazaya istek gönderilmedi.

Satıcı açığı nasıl kapattı?

3.7.9'da bağlantı akışı ayrı bir sınıfa taşındı ve geri çağrı ucu baştan yazıldı. Yeni kodda değerlerin hem türü hem içeriği denetleniyor:

// classes/woo-swish-oauth-connection.php (3.7.9)
if ( ! is_object( $body ) || ! self::nonempty_string( $body->nonce ?? null )
    || ! self::nonempty_string( $body->uuid ?? null ) || ! self::nonempty_string( $body->refresh_token ?? null ) ) {
    return false;
}
...
if ( ( $settings['connection_type'] ?? '' ) !== '_service'
    || ! is_array( $pending ) || ! self::nonempty_string( $pending['nonce'] ?? null )
    || ! is_int( $pending['expires'] ?? null ) || $pending['expires'] <= time()
    || ! self::nonempty_string( $uuid ) || $body->uuid !== $uuid || ( $pending['uuid'] ?? null ) !== $uuid
    || ! hash_equals( $pending['nonce'], $body->nonce ) ) {
    return false;
}

Düzeltme, iki kusurun ikisine de cevap veriyor. Gelen değerlerin boş olmayan metin olması şart koşuluyor, böylece true, null ya da sayı gönderilemiyor. Bekleyen bir bağlantı akışı yoksa ya da süresi geçmişse istek reddediliyor. Hesap kimliği katı karşılaştırmayla (!==), tek kullanımlık değer ise zamanlamaya dayanıklı hash_equals() ile denetleniyor. Tek kullanımlık değer başarılı bağlantıdan sonra siliniyor ve aynı sürümde bağlantıyı başlatan yönetici ucuna da yetki kontrolü (current_user_can( 'manage_woocommerce' )) eklendi.

Geliştiriciler için ders ne?

  • Gizli bir değeri ya da kimliği karşılaştırırken PHP'de == kullanmayın. Kimlik için ===, gizli değer için hash_equals() kullanın ve ikisinden önce değerin beklenen türde olduğunu denetleyin.
  • JSON gövdesi okuyan uçlarda gönderenin türü de seçtiğini unutmayın. Form verisinde her şey metin gelir; JSON'da mantıksal değer, sayı ve null da gelebilir.
  • “Beklenen değer yoksa” durumunu ayrıca ele alın. Bekleyen bir akış yokken karşılaştırma yapmak yerine isteği reddetmek gerekir.

Site sahibi ne yapmalı?

  • Eklentiyi 3.7.9 ya da sonraki bir sürüme güncelleyin. Sürüm notuna göre güncellemeden önce başlatılıp bitirilmemiş bağlantıların yeniden başlatılması gerekiyor.
  • Güncellemeden sonra Swish bağlantısını kesip yeniden kurmak, yerinde duran jetonun gerçekten size ait olmasını garanti eder.
  • Ödeme kayıtlarınızda Swish ödemelerinin açıklanamayan şekilde başarısız olduğu bir dönem varsa bunu ödeme servisiyle birlikte inceleyin.

Bunun hukuki bir boyutu var mı?

Açık bir kişisel veri sızıntısı değildi; değiştirilen şey mağazanın ödeme servisine erişim kimliğiydi. Ama e-ticaret sitesi müşterilerinin ad, adres, telefon ve sipariş bilgilerini işleyen bir veri sorumlusudur ve verilere hukuka aykırı erişilmesini önlemek için gerekli teknik ve idari tedbirleri almak zorundadır (KVKK m. 12/1). Ödeme altyapısının kimlik bilgilerini koruyan bileşenler bu tedbirlerin merkezinde yer alır. Veri sorumlusu, verileri kendi adına işleyen kişilerle de bu tedbirlerden birlikte sorumludur (KVKK m. 12/2). Eklentiyi ve aracı ödeme servisini seçmek ve güncel tutmak bu yüzden yalnız teknik bir tercih değil, hukuki bir yükümlülüğün parçasıdır.

Zaman çizelgesi

  • 11 Ekim 2026: Açık deneme kurulumunda doğrulandı ve Patchstack üzerinden bildirildi.
  • 12 Ekim 2026: Patchstack bulguyu doğruladı ve üreticiye iletti.
  • 18 Ekim 2026: Swish for WooCommerce 3.7.9 yayımlandı.
  • 21 Ekim 2026: CVE-2026-111835 yayımlandı.

Değerlendirmem

Bu açıkta saldırganın yaptığı tek şey, beklenen metinlerin yerine true yazmaktı. Gevşek karşılaştırma PHP'nin eski ve iyi bilinen bir tuzağıdır; yine de ödeme altyapısının en hassas noktasında duruyordu. Üreticinin düzeltmesi, eksik olan tek koşulu eklemekle yetinmeyip bağlantı akışını baştan kurduğu için iyi bir örnektir: türü, süreyi, tek kullanımı ve karşılaştırma yöntemini birlikte ele alıyor.

Kaynaklar

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

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