Kendi kimlik doğrulama katmanımı yazmanın bir gurur kaynağı olduğunu düşünürdüm. JWT'leri anlıyorsanız ve bir şifreyi hash'leyebiliyorsanız, ne kadar zor olabilir ki? Bir Node backend kurdum, token'ları oluşturdum ve işi bitirdim sandım. Kod çalışıyordu. Testler geçiyordu. Sonra gerçek saldırıların nasıl gerçekleştiğine dair okumaya başladım ve tüm temellerim sarsıldı. Enjeksiyon, numaralandırma (enumeration) ve yan kanal sızıntıları hakkındaki her bölüm, içimi kaplayan bir huzursuzlukla beni editörüme geri gönderdi. Kimlik doğrulama sistemimin sadece boşlukları yoktu; kendi ellerimle kurduğum dört tane ardına kadar açık kapısı vardı. İşte bulduklarım ve tam olarak neleri değiştirdiğim.

String Birleştirme Yoluyla SQL Injection

İlk hata, kitaptaki en eski numaraydı. Kullanıcı girdisini alıyor ve doğrudan SQL dizelerine yerleştiriyordum. Giriş (login) rotamda, istek gövdesinden e-postayı alıyor ve onu SELECT * FROM users WHERE email = '${email}' gibi bir sorguya ekliyordum. Ön yüzü (frontend) ben kontrol ettiğim için bu zararsız geliyordu. Bu tehlikeli bir varsayımdır. Bir saldırganın sizin ön yüzünüze ihtiyacı yoktur. Tek bir özel olarak hazırlanmış POST gövdesi, o giriş kontrolünü bir veri ihlaline veya silinmiş bir veritabanına dönüştürebilir. ' OR '1'='1 gibi bir payload veya daha kötüsü, tabloları silen bir stacked query gönderilirse ve string ham (raw) olarak yürütülürse, verileriniz gider. Veritabanına, benim kodum ile saldırganın verisi arasında ayrım yapabileceği hiçbir yol bırakmamıştım.

Çözüm daha fazla girdi doğrulaması veya stringleri elle kaçırmak (escaping) değildi. Gerçek çözüm parametreli sorgulardı (parameterized queries). node-postgres kütüphanesine geçtim ve $1 gibi yer tutucular kullanmaya başladım. Sorgu bir şablona dönüşüyor: SELECT * FROM users WHERE email = $1. Sürücü, SQL'i ve değerleri ayrı kanallar üzerinden gönderir. Veritabanı, hangi karakterleri içerirse içersin, girdiyi kesinlikle sadece veri olarak işler. Bu tek değişiklik, tüm enjeksiyon saldırıları sınıfını kapatır. Okuması daha basit, bakımı daha kolaydır ve her WHERE ifadesi yazdığınızda bir regex büyücüsü olma yükünü ortadan kaldırır.

Hata Mesajları Yoluyla E-posta Numaralandırma (Email Enumeration)

İkinci hatam iyi bir kullanıcı deneyimi (UX) gibi görünüyordu. Bir kullanıcı yanlış e-posta yazdığında User not found döndürüyordum. E-postayı doğru girip şifreyi yanlış girdiğinde ise Incorrect password döndürüyordum. Bu yardımcı bir yaklaşım gibi hissettiriyordu. Ancak aynı zamanda saldırganlar için bir keşif aracıydı. Numaralandırma betikleri, giriş uç noktanıza (endpoint) binlerce e-posta adresiyle saldırabilir. Yanıt gövdesi veya durum kodu, hesabın var olup olmamasına bağlı olarak değişiyorsa, betik kullanıcılarınızın doğrulanmış bir listesini oluşturabilir. Bu liste; credential stuffing, hedefli kimlik avı (phishing) ve daha ileri brute-force girişimleri için temel oluşturur.

Kullanıcı dostu olmanın bazen güvenlik karşısında geri adım atması gerektiğini kabul etmem gerekiyordu. Her başarısız giriş yolunu tam olarak aynı diziyi döndürecek şekilde değiştirdim: Invalid credentials. İpucu yok. Hata yanıtında dallanan bir mantık yok. E-posta eksik olsa da, şifre yanlış olsa da veya hesap kilitli olsa da metin aynı kalıyor. Bu durum kayıt ve şifre sıfırlama akışları için de geçerlidir; bir adresin sisteminizde zaten olup olmadığını ifşa etmeyin. Tek bir genel mesaj, saldırganların bağımlı olduğu bir bilgi sızıntısını ortadan kaldırır.

Şifre Karşılaştırmasında Zamanlama Saldırıları (Timing Attacks)

Üçüncü hata görünmezdi. Ben...