Kendi Uygulamamı Hackledim: Kredi Sistemini İstemciden Sunucuya Taşıma Rehberi

Kendi Uygulamamı Hackledim: Bir Masaüstü Yazılımında Kredi Sistemini İstemciden Sunucuya Taşıma Hikayesi

Bir gün kendi ürününüzü kırmaya karar verirsiniz. Ben de öyle yaptım. Elimde NeonCore vardı: Windows üzerinde çalışan, dosyalarınızı buluta göndermeden yapay zekayla iş gördüren otonom bir masaüstü asistanı. Kullanıcılar "yakıt" adını verdiğim bir kredi sistemiyle çalışıyordu. Her işlem bir miktar yakıt harcıyordu. Sistem çalışıyordu, satış yapıyordu, müşteriler memnundu. Ama içimde bir his vardı: ya biri bu krediyi kendi eliyle sınırsıza çekerse?

Bu soruyu test etmek için oturup küçük bir betik yazdım. Amacım kötü niyet değildi; amacım kendi savunmamı sınamaktı. Sonuç, gecemi uykusuz bırakan türdendi. Bu yazıda, bir masaüstü uygulamasında para değeri taşıyan bir sistemi neden asla istemci tarafında tutmamanız gerektiğini, benim bunu nasıl fark ettiğimi ve tüm mimariyi nasıl yeniden kurduğumu, gerçek kararlar ve gerçek kod parçalarıyla anlatacağım. Eğer bir gün kendi ürününüzde ödeme, kredi, lisans veya kota gibi bir değer sistemi kuracaksanız, bu deneyimin size saatlerce baş ağrısı kazandıracağını düşünüyorum.

Neden Bu Konu Türkiye'deki Geliştiriciler İçin Ayrı Bir Önem Taşıyor

Türkiye'de tek başına ürün geliştirip satan bir yazılımcıysanız, muhtemelen benim gibi hem kod yazıyor hem pazarlama yapıyor hem de KVKK yükümlülüklerini kendi omuzlarınızda taşıyorsunuz. Büyük ekiplerin ayrı bir güvenlik departmanı, ayrı bir hukuk ekibi vardır. Bizde o lüks yok. Bu yüzden mimari kararlarınızın güvenlik sonuçlarını ilk günden görmek zorundasınız, çünkü bir açığı fark ettiğinizde onu düzeltecek olan da yine sizsiniz.

Bir diğer nokta da şu: yabancı kaynaklar genelde "kredi sistemini backend'de tut" der geçer, ama Shopier ile satış yapan, ödemesini Türkiye'den alan, lisans kodunu e-postayla gönderen bir yapının gerçek detaylarını anlatmaz. Ben bu yazıda tam olarak o boşluğu doldurmak istiyorum: yerel pazarda satılan gerçek bir üründe, gerçek bir para sistemini nasıl güvenli hale getirdiğimi.

Sahne: Kredi Nerede Duruyordu

İlk mimaride her şey kullanıcının bilgisayarındaydı. Uygulama açıldığında kullanıcıya on ücretsiz kredi veriliyordu. Bu kredi, kullanıcının bilgisayarındaki bir dosyada, şifreli olarak tutuluyordu. "Şifreli" kelimesi kulağa güven verici geliyor, değil mi? Ben de öyle sanmıştım. Şifreleme için AES-256 kullanıyordum, anahtar da sunucu kodunun içinde duruyordu.

İşte tam burada ölümcül hata gizliydi. Masaüstü uygulaması dediğiniz şey, kullanıcının bilgisayarına kurulan ve orada çalışan bir programdır. Yani o programın içindeki her şey, teoride kullanıcının erişebileceği bir yerdedir. Şifreleme anahtarını kodun içine gömdüğünüzde, o anahtarı kullanıcıdan gizlediğinizi sanırsınız; oysa sadece bir kilidi, kilidin anahtarını da yanına asarak saklamış olursunuz.

Şifreleme anahtarım kod içinde açık metin olarak duruyordu. Bunun anlamı şuydu: yeterince meraklı biri, kurulu uygulamanın dosyalarını açar, anahtarı bulur, kredi dosyasını çözer, içindeki sayıyı doksan dokuz bin yapar ve tekrar şifreleyip yerine koyardı. Uygulama açıldığında hiçbir şeyden şüphelenmez, kullanıcının doksan dokuz bin kredisi olduğunu görür ve mutlulukla çalışmaya devam ederdi.

Kendi Ürünümü Kırdığım An

Bu senaryonun gerçekten mümkün olup olmadığını görmek için hack-test adını verdiğim bir betik yazdım. Betik tam olarak yukarıda anlattığım adımları uyguluyordu: kod içindeki anahtarı kullanarak kredi dosyasını çözdü, kredi değerini elle değiştirdi, tekrar şifreledi ve yerine yazdı. Sonra uygulamayı açtım.

Karşımda, hiç ödeme yapmadan elde edilmiş devasa bir kredi bakiyesi duruyordu. Uygulama bunu tamamen meşru kabul ediyordu. Çünkü uygulamanın gözünde "kredi ne kadarsa o kadardır"; kredinin nereden geldiğini sorgulayacak bir merci yoktu. Karar mercii, saldırganın kontrolündeki dosyanın kendisiydi.

O an anladım ki sorun şifrelemenin zayıflığı değildi. Sorun, para değeri taşıyan bir kararın yanlış yerde alınmasıydı. Kredinin ne kadar olduğuna kullanıcının bilgisayarı karar veriyordu. Oysa bu kararı, kullanıcının asla dokunamayacağı bir yerin, yani sunucunun vermesi gerekiyordu. Güvenlikte altın kural şudur: istemciye asla güvenme. İstemci, yani kullanıcının elindeki program, her zaman değiştirilebilir. Gerçek karar her zaman senin kontrolündeki sunucuda alınmalıdır.

İkinci Açık: Kimliksiz Kapı

Betiği yazarken ikinci bir sorun daha gözüme çarptı. Uygulama, yapay zeka isteklerini bir ara sunucu üzerinden geçiriyordu. Bu ara sunucunun görevi, gerçek yapay zeka anahtarını gizlemekti; mantıklı bir tasarım. Ama bu ara sunucuya hiçbir kimlik doğrulaması koymamıştım. Yani adresini bilen herkes, o kapıdan girip benim yapay zeka anahtarımı kullanarak bedavaya işlem yaptırabilirdi. Fatura bana gelirdi, kullanan başkası olurdu.

Üstelik veritabanı kuralları da fazlasıyla gevşekti. Lisans kodlarının tutulduğu bölüme, adresi bilen herkes okuma ve yazma yapabiliyordu. Bu, sadece bir kredi sorunu değil, aynı zamanda bir gizlilik sorunuydu; çünkü aynı veritabanında müşteri e-postaları da duruyordu. KVKK'nın veri güvenliği yükümlülüğü açısından bu, kapatılması gereken ciddi bir açıktı.

Çözümün İskeleti: Geçit Sunucusu

Çözüm netti: krediyi ve kredi kararını cihazdan alıp sunucuya taşımak. Bunun için bir geçit sunucusu kurdum. Cloudflare Workers üzerinde çalışan, üç temel kapısı olan hafif bir sunucu. Bu sunucu, uygulamayla veritabanı arasında tek yetkili aracı olacaktı. Uygulama artık ne krediye doğrudan dokunabilecek, ne de yapay zekaya doğrudan gidebilecekti; her şey bu geçitten geçecekti.

Üç kapının işlevleri şöyleydi. Birinci kapı ilk açılış içindi: uygulama ilk kez çalıştığında bir cihaz kimliği üretir, sunucu bu kimlik için veritabanında bir hesap açar ve on ücretsiz kredi tanımlar. İkinci kapı lisans kodu içindi: kullanıcı satın aldığı kodu girdiğinde, sunucu kodun geçerli ve kullanılmamış olduğunu doğrular, krediyi hesaba ekler ve kodu kullanılmış olarak işaretler. Üçüncü kapı ise sohbet içindi: her yapay zeka isteği bu kapıdan geçer, sunucu önce krediyi kontrol edip düşer, sonra isteği yapay zekaya iletir. Kritik nokta buydu: kredi kararı artık sunucuda alınıyordu.

İmzalı Jeton: Kurcalansa da İşe Yaramaz

Peki uygulama sunucuya kim olduğunu nasıl kanıtlayacaktı? Burada imzalı jeton yöntemini kullandım. Uygulama ilk açıldığında sunucu ona bir jeton verir. Bu jeton, kullanıcının kimliği ile sunucudaki gizli bir anahtarın birleştirilip imzalanmasıyla üretilir. İmza yöntemi HMAC-SHA256'dır.

İşin güzel tarafı şu: jeton kullanıcının bilgisayarında saklanır, kullanıcı isterse onu açıp içine bakabilir, hatta değiştirmeyi deneyebilir. Ama değiştirdiği anda imza tutmaz. Çünkü imzayı yeniden üretebilmek için sunucudaki gizli anahtara ihtiyaç vardır ve o anahtar hiçbir zaman kullanıcının eline geçmez. Sunucu, gelen her jetonun imzasını kendi anahtarıyla yeniden hesaplar; imza uyuşmuyorsa jetonu reddeder.

Jeton doğrulamasının çekirdeği şuna benziyordu:

javascript

async function verifyToken(token, secret) {
  const parts = (token || "").split(".");
  if (parts.length !== 2) return null;
  const [payload, sig] = parts;
  const expected = await hmacSign(payload, secret);
  if (sig !== expected) return null;
  try { return atob(payload); } catch { return null; }
}

Bu birkaç satır, eski mimarideki en büyük açığı anlamsız kılıyordu. Artık kullanıcının bilgisayarında kurcalanacak bir kredi verisi yoktu. Elinde sadece imzalı bir jeton vardı ve o jetonu kurcalarsa sunucu onu kapıdan içeri almıyordu.

Kredi Kararı Neden Sunucuda Alınmalı

Sohbet kapısının mantığı, tüm sistemin kalbiydi. Uygulama bir istek gönderdiğinde, sunucu önce jetonu doğrular, sonra o kimliğe ait krediyi veritabanından okur. Kredi yetersizse istek reddedilir. Kredi yeterliyse sunucu krediyi düşer ve isteği yapay zekaya iletir. Yani kredinin ne zaman, ne kadar düşeceğine uygulama değil, sunucu karar verir.

Bu tasarım, ilginç bir yan etki de doğurdu. Uygulamanın otonom yapısı gereği, tek bir kullanıcı komutu bazen birden fazla yapay zeka çağrısı tetikliyordu; önce hangi aracın kullanılacağına karar veriliyor, araç çalışıyor, sonra sonucu yorumlamak için ikinci bir çağrı yapılıyordu. Her çağrı gerçek bir maliyet doğurduğu için, her çağrıyı bir kredi olarak saymaya karar verdim. Böylece basit bir sohbet bir kredi, dosya okuyan ya da web araması yapan bir komut birkaç kredi harcıyordu. Bu, hem maliyetle örtüşen hem de suistimale kapalı bir modeldi; çünkü "bu devam çağrısı, saymayın" gibi bir kaçış yolu bırakmıyordu. Yapay zeka çağrılarının maliyetini ve akışını yönetmenin ayrıntılarına, React ile yapay zeka entegrasyonunda streaming, bellek ve maliyet yönetimi rehberinde daha derinlemesine değinmiştim.

Model seçimini de sunucuda sabitledim. Uygulama isteğinde hangi modeli istediğini yazsa bile, sunucu bunu yok sayıp kendi belirlediği modeli kullanıyordu. Böylece kullanıcının, sunucuya pahalı bir model seçtirerek maliyeti şişirmesi de imkansız hale geldi.

Veritabanını Kilitlemek

Son adım veritabanını kilitlemekti. Kuralları öyle ayarladım ki hiçbir istemci doğrudan okuma ya da yazma yapamıyor. Sadece geçit sunucusu, elindeki gizli anahtarla veritabanına erişebiliyor. Bu, hem kredi verilerini hem de müşteri e-postalarını dışarıya tamamen kapattı.

Kilit kuralının kendisi son derece basitti:

json

{
  "rules": {
    ".read": false,
    ".write": false
  }
}

Ama bu basit kuralı uygularken bir tuzağa dikkat etmek gerekiyordu. Satış otomasyonum, yani Shopier'den gelen siparişi alıp lisans kodu üreten ve müşteriye e-posta gönderen sistem, aynı veritabanına yazıyordu. Eğer veritabanını, bu otomasyonu kimlikli hale getirmeden kilitleseydim, otomasyon sessizce kırılırdı. Müşteri parasını öder, kodunu alamazdı. Bu yüzden kilitleme adımını en sona bıraktım ve önce satış otomasyonunun da gizli anahtarla, kimlikli biçimde yazdığından emin oldum. Para sistemlerinde sıra, bazen kodun kendisi kadar önemlidir.

Yapılandırma Odaklı Mimarinin Bu Süreçteki Rolü

Bu dönüşümü göreceli olarak sancısız atlatmamın bir sebebi de baştan beri uyguladığım bir prensipti: veri katmanını arayüz katmanından ayrı tutmak. NeonCore'da hangi paketin kaç kredi verdiği, hangi adresin kullanıldığı, hangi modelin çağrıldığı gibi değerler, arayüz kodunun içine serpiştirilmiş sabitler değildi. Bunlar tek bir yerde, açıkça tanımlıydı. Bu ayrım, geçit sunucusuna geçerken işimi çok kolaylaştırdı; çünkü değiştirmem gereken yerler dağınık değil, toplu haldeydi.

Aynı prensibi geçit sunucusunun kendisinde de uyguladım. Veritabanına yazan ve okuyan tüm kodu tek bir fonksiyon çiftinde topladım. Böylece ileride, örneğin daha güvenli bir kimlik doğrulama yöntemine geçmek istersem, onlarca yeri tek tek değiştirmek zorunda kalmayacağım; sadece o iki fonksiyona dokunmam yetecek. Veritabanı erişimini tek bir kapıdan geçirmek, hem güvenlik denetimini kolaylaştırır hem de gelecekteki değişiklikleri tek noktaya indirir.

Bu yaklaşımı özellikle vurgulamak istiyorum, çünkü tek başına çalışan geliştiricilerin en sık düştüğü tuzaklardan biri, değerleri kodun her yerine dağıtmaktır. İşler yolundayken bu bir sorun gibi görünmez. Ama bir gün bir şeyi değiştirmeniz gerektiğinde, o değerin kaç farklı yerde geçtiğini bulmaya çalışırken saatler harcarsınız. Verinin nerede durduğunu ve kararın nerede alındığını en baştan net tutmak, ileride yapacağınız her güvenlik ve bakım çalışmasını kolaylaştırır.

Otomasyonu Bozmadan Kilitlemek

Kilitleme adımında karşılaştığım en öğretici an, satış otomasyonuyla ilgili olandı. NeonCore'un satış tarafı tamamen otomatikti: bir müşteri Shopier üzerinden paket satın aldığında, arka planda bir işlem tetikleniyor, bu işlem benzersiz bir lisans kodu üretiyor, kodu veritabanına yazıyor ve müşteriye e-posta ile gönderiyordu. Bu otomasyonu nasıl kurduğumu daha önce Shopier webhook ile otomatik lisans anahtarı gönderme yazısında adım adım anlatmıştım. Ben uyurken bile satış olsa, müşteri kodunu saniyeler içinde alıyordu. Bu otomasyon, ürünün en değerli parçalarından biriydi.

Veritabanını kilitlemeye karar verdiğimde, bu otomasyonun veritabanına nasıl yazdığını kontrol ettim ve gördüm ki kimlik doğrulaması olmadan yazıyor. Yani kuralları hemen kilitleseydim, otomasyon çalışmaya devam edemez, satış gelir ama kod üretilemezdi. Bu, bir para sisteminde yaşanabilecek en kötü senaryolardan biridir: müşteri öder, ürünü alamaz.

Bu yüzden sırayı dikkatlice kurdum. Önce satış otomasyonunu, veritabanına gizli anahtarla, yani kimlikli biçimde yazacak şekilde güncelledim. Bu değişikliği veritabanı hâlâ açıkken yaptım, çünkü kimlikli yazma açık kuralda da sorunsuz çalışır. Otomasyonun yeni haliyle düzgün çalıştığından emin olduktan sonra, ancak o zaman veritabanını kilitledim. Bir de küçük ama önemli bir sağlamlaştırma ekledim: eğer kod yazma işlemi herhangi bir sebeple başarısız olursa, otomasyon artık müşteriye e-posta göndermiyor ve satış sistemine bir hata bildiriyor. Böylece müşterinin çalışmayan bir kod alması ihtimalini de ortadan kaldırdım.

Adım Adım İlerlemenin Değeri

Tüm bu dönüşümü tek seferde yapmadım. Bunun bir para sistemi olduğunu bir an bile unutmadım. Her adımı tek tek, izole biçimde test ettim. Önce sadece jeton üreten iskeleti kurdum ve doğruladım. Sonra veritabanı bağlantısını ekledim, tekrar test ettim. Sonra lisans kodu kapısını ekledim, bir test kodu üretip kredinin gerçekten arttığını gördüm. Sonra sohbet kapısını ekledim. En son, her şey çalıştığını kanıtladıktan sonra veritabanını kilitledim. Ve uygulamanın kendisine, yani kullanıcının elindeki koda, en son ve en dikkatli biçimde dokundum.

Bu yaklaşımın neden önemli olduğunu vurgulamak isterim. Bir para sistemini baştan sona bir kerede değiştirmeye kalkarsanız, bir yerde hata yaptığınızda hatanın nereden kaynaklandığını bulmak neredeyse imkansız hale gelir. Küçük parçalar halinde ilerlediğinizde ise her adım kendi başına doğrulanır, bir sonraki adıma sağlam bir zemin üzerinde geçersiniz. Canlı müşterisi olan bir üründe bu, lüks değil zorunluluktur.

Sonuç: Neyi Kazandım

Dönüşüm bittiğinde tablo tamamen değişmişti. Kredi kararı artık sunucuda alınıyordu, cihazda kurcalanacak bir kredi verisi kalmamıştı. Bu, en başta yazdığım hack-test betiğini tamamen işlevsiz bıraktı; çünkü betiğin değiştireceği bir dosya artık yoktu. Yapay zeka kapısı kimlikli hale gelmişti, adresini bilmek artık yeterli değildi. Veritabanı kilitliydi, müşteri e-postaları dışarıya kapalıydı. Eski, kimliksiz ara sunucu emekliye ayrılmış, ona ait anahtar iptal edilmişti.

En çarpıcı sonuç şuydu: eski mimarideki o meşhur şifreleme anahtarı, hâlâ kodun içinde duruyordu ama artık hiçbir önemi yoktu. Çünkü o anahtar artık sadece kullanıcının kendi profilini ve sohbet geçmişini şifreliyordu; parayla hiçbir ilişkisi kalmamıştı. Açık, açık olmaktan çıkmıştı çünkü koruduğu değerli şey oradan alınmıştı.

Bu deneyimden çıkardığım en büyük ders şu oldu: güvenlik, çoğu zaman daha güçlü bir şifreleme meselesi değil, kararın doğru yerde alınması meselesidir. Kredinizi ne kadar güçlü şifrelerseniz şifreleyin, o kredinin ne kadar olduğuna kullanıcının bilgisayarı karar veriyorsa, sistem kırılabilir. Kararı sunucuya taşıdığınız an, şifreleme zaten ikincil bir detay haline gelir.

Eğer siz de kendi ürününüzde bir kredi, kota, lisans ya da ödeme sistemi kuruyorsanız, size tavsiyem nettir: değeri istemcide tutmayın, kararı istemciye bırakmayın. Kendi ürününüzü, bir saldırganın gözüyle bir kez kırmayı deneyin. Bu, uykunuzu kaçırabilir; ama o uykusuz gece, ilerideki çok daha kötü sürprizlerden sizi koruyacaktır.

Bu güvenlik dönüşümünün sonucunu bizzat deneyimlemek isterseniz, tüm bu altyapının üzerine kurulu NeonCore'u buradan indirip ücretsiz kredilerle deneyebilirsiniz. Dosyalarınız bilgisayarınızdan çıkmadan, kredi sisteminiz ise güvenle sunucu tarafında çalışır.