QR login, mobil cihazlar için yaygın bir özellik gibi görünse de, güvenlik açısından ciddi riskler barındırmaktadır. Tarayıcıda bir QR kodu gösterilir, bu kod bir mobil cihazla tarandığında zaten giriş yapmışsanız web oturumu otomatik olarak açılır. WhatsApp Web, Telegram Web, Steam. Herkes bu özelliği kullandı; ancak arka planda neler olup bittiğini kimse düşünmez.
Mevcut bir uygulamada çalışır bir versiyonunu zaten geliştirmiştim. LaraFoundry çekirdeğine çok cihazlı giriş eklemek gerektiğinde, her modül için yaptığım gibi gerçek kodu çıkardım, yani blog yazılarından sıfırdan yazmak yerine, uygulamada gerçek kullanıcılarla temas etmiş olan bir kodu tahlil ettim. Ancak bir tuhaflık var ve bu yazının asıl noktası bu: çalışır durumdaki kod, güvenli kod ile aynı şey değildir. Bağışlanan sürüm iyi çalışıyordu ama yollamak istemediğim on bir eksiklik taşıyordu.
İşte her bir güvenlik açığı ve bunların nasıl düzeltildiği.
Akış, bir paragrafta
Akış, bir paragrafta
Tarayıcı (giriş yapmamış bir misafir) arka uçtan giriş isteği talep eder ve bunu bir QR kodu olarak render eder. İkinci bir cihaz, zaten yetkilendirilmiş olarak, kodu tarar ve onaylamak için bir doğrulama son noktasına (endpoint) erişir. Bu arada, tarayıcı talep onaylandığında kullanıcıyı oturum açtırmak için sürekli kontrol eder. Üç ya da daha fazla son nokta: oluştur, doğrula, kontrol et. Basit bir yapı, birçok keskin köşe içeriyor.
On bir
On bir
| # | Bağışlayan bunu yaptı | Çekirdek bunu yapıyor |
|---|---|---|
| 1 | Ham token’i DB’de sakladı ve bunu QR URL’sine ekledi | SHA-256 hash’ini saklar; düz metin yalnızca QR görüntüsünde yaşar |
| 2 | Herhangi bir son noktada hız sınırı yok | Oluştur, kontrol et ve doğrula üzerinde throttling uygular (doğrulama zorla kırma yüzeyidir) |
| 3 | axios.get(decodedText) QR’nin çözülenine ne olursa olsun erişim sağlar | Tarayıcıdan okunmuş URL’yi doğrular (aynı köken ve tam doğrulama yolu) talep yapılmadan önce |
| 4 | TTL hiçbir zaman sona ermezdi | Oluşumdan itibaren mutlak bir sınır koyar, böylece bir kod yeniden yenilense bile sona erer |
| 5 | Sona ermiş satırlar sonsuz bir şekilde birikir | Host’un zamanladığı bir prune komutu vardır |
| 6 | Auth::login() sonra session()->regenerate() | Önce yeni al, sonra giriş yap (oturum sabitleme) |
| 7 | Deneme kayıtları tutulmazdı | Her onay, başarısızlık ve blok giriş etkinlik günlüğüne yazılır |
| 8 | Sorgulandığı sütunlarda hiçbir dizin yoktu | Kontrol ve prune yargıları üzerinde dizinler vardır |
| 9 | URL’den ID’yi şifreledi, bu nedenle kötü bir değer 500 hatası verecekti | Hiçbir şifreleme yapılmaz; kötü bir kod temiz bir 404 döner |
| 10 | Token ve id’yi oturumda iki kez şifreledi | Yalnızca satır ID’sini bir kez saklar |
| 11 | Son kullanım kontrolünde açıklanmayan bir subSeconds(13) uygulaması vardı | Gitti; basit bir expires_at > now() |
Üçü, herkesin sıkça yanlış yaptığı, kodda gösterilmesi gereken yerlerdir.
Açık 1: token düz metin olarak veri tabanında
Açık 1: token düz metin olarak veri tabanında
Bağışlayan bir UUID oluşturdu, olduğu gibi sakladı ve QR URL’sine gömdü. Eğer veri tabanınız sızarsa, her geçerli QR kodu da sızar. Token, bir bearer secret olduğundan, aynı muameleyi görmelidir; onu hash’leyin, hash’leri karşılaştırın, asıl formunu saklamayın.
// generate(): düz metin yalnızca QR'de yaşar, DB'de sadece hash vardır
$plain = (string) Str::uuid();
$signInRequest = SignInRequest::create([
'token' => hash(, ),
=> now()->addMinutes(->ttlMinutes()),
=> ->ip(),
=> ->userAgent(),
]);
// verify(): URL'den gelen token hash'ini kullanarak kaydı bul
$signInRequest = SignInRequest::query()
->where(, )
->where(, hash(, ))
->where(, false)
->where(, , now())
->first();
Aşırı şifreleme hamlesinden kurtulmanın güzel bir yan etkisi vardır: geriye atılacak bir şey kalmaz. Bağışlayan, ID’yi Crypt::encrypt() ile sarmalamıştı, dolayısıyla biçimi bozuk bir değer 500 hatası veriyordu (açık 9). Hash’leme, bir dize karşılaştırmadır. Yanlış kod sadece eşleşmez ve temiz bir 404 döner.
Açık 3: tarayıcı, çözülen şeyi fetch etti
Açık 3: tarayıcı, çözülen şeyi fetch etti
Bu durum beni çok rahatsız etti. Mobil taraf bunun böyleydi:
// Bağışlayan: kameranın çözdüğü şeyin peşinden koş
const onScanSuccess = async (decodedText) => {
await axios.get(decodedText)
location.reload()
}
Bir QR kodu saldırgana kontrol edilen bir girdi demektir. Kameranızı kötü bir koda çevirirseniz, sayfa içindeki her URL’ye istek gönderecektir. Bu, sunucu tarafında istek sahtekarlığı ve açık yönlendirme vektörü sağlar. Çözüm, yalnızca kendi doğrulama son noktamızı fetch etmektir; aynı köken, doğru yol, gerçek şema olmalıdır:
function safeVerifyUrl(decoded) {
let url
try {
url = new URL(decoded, window.location.origin)
} catch {
return null
}
// blob:/data:/javascript: gibi şeyleri yok say
if (url.protocol !== 'https:' && url.protocol !== ) return null
if (url.origin !== window.location.origin) return null
return /^(?:\/[^/]+)?\/larafoundry\/qr\/verify\/d+\/[^/]+$/.test(url.pathname)
? url.toString()
: null
}
URL ayrıştırıcısı kodlamayı ve yol geçişini normalleştirir, böylece ..%2f gibi hileler geçmeyecek. Köken, canlı kökenle eşleştirilmektedir; dize eşleşmesi yapmaz, bu nedenle localhost.evil.com ve kullanıcı bilgileri yöntemleri ([email protected]) geçemez. Bir inceleyen, daha sonrasında bunu geçmeye çalışan 25 vektör sundu ve her tehlikeli olan geri çevrildi.
Açık 6: önce giriş yap, sonra sor
Açık 6: önce giriş yap, sonra sor
Bağışlayan kullanıcıyı oturum açtırdı ve ardından oturum yeniledi. Bu sıra, bir oturum sabitleme çağrısını davet ediyor: bir saldırgan oturum kimliğini girişten önce sabitleyip, kullanıcıya bağlanmadan geçerli bir kimliğe sahip olur. Sıraları değiştirmek, odadaki bağlantı kimliğini atarak, kullanıcı ona bağlı olmadan önce geçersiz hale getirir.
// poll(): yeni oturum kimliği KİŞİLİK bağlanmadan önce
$request->session()->regenerate();
Auth::guard()->login();
$signInRequest->delete(); // tek kullanımlık: onaylanmış kod yeniden oynatılamaz
delete() kendi küçük bir düzeltmesidir. Bağışlayan, onaylı satırı yerinde bıraktı, dolayısıyla aynı kod bir daha sorgulanabilirdi. Tek bir kullanım, ardından yok.
Bağışlayanın asla sahip olmadığı şey: süper-admin engeli
Bağışlayanın asla sahip olmadığı şey: süper-admin engeli
Platformda bir kural vardır: operatör hesabı QR ile oturum açamaz. Bağışlayan ham bir is_admin sütununu kontrol etti. Çekirdek, “bu platformun süper yöneticisi mi” sorusu için tek bir çözücüye sahip ve bu, bayraktan bağımsız bir e-posta izin listesinin üstüne eklenmiştir. Böylece ters çevrilmiş bir sütun yalnızca bunu sağlamaz. QR doğrulama, ikinci bir kontrol icat etmektense bu tek çözücüyü yeniden kullanır:
if ($this->visitorStatus->isAdmin($approver)) {
// denetlendi, sonra reddedildi
return response()->json([=> __()], 403);
}
Güvenlik kararı için tek bir gerçeklik kaynağı, iki kaynaktan daha iyidir; çünkü bu iki kaynak bir refaktör sırasında uyumsuz hale gelebilir.
İçerideyken: sayfa veya modal
İçerideyken: sayfa veya modal
Aynı aşama, küçük ama ilgili bir yaşam kalitesi iyiliğini sundu. Kimlik doğrulama ekranları (giriş, kayıt, unuttum, sıfırlama) artık bir tam sayfa veya ana içeriğin üzerine bir modal olarak render edilebiliyor, bu bir yapılandırma değerine bağlı:
=> env(, ), // sayfa | modal
Varsayılan değer page‘dir, bu nedenle içeriğinde herhangi bir değişiklik yapılmadan, opt-in yapılmamış olan herkes için hiçbir değişiklik olmaz. Bağışlayan yalnızca modal içeriyordu, çekirdek yalnızca sayfalar içeriyordu. Hiçbiri bu değişkenlikte bulunmuyordu. Şimdi bir yapılandırma anahtarı, görünümü seçiyor ve aynı ekran bileşeni iki farklı şekilde render ediyor. Bilinmeyen bir değer geri dönüş yapmadan page‘ye düşer, böylece bir yazım hatası aşağıda tanımsız bir yüzey üretemez.
Kanıt, hissiyat değil
Kanıt, hissiyat değil
Çekirdekteki her modül, Pest testleri ile birlikte gelir ve bir güvenlik özelliği en yoğun şekilde test edilir. QR arka uç, tam iki cihazlı el sıkışmayı tek süreç içinde çalıştıran bir teste sahip (iki bağımsız oturum): bir misafir oluşturur, bir onaylayıcı doğrular, misafir kontrol eder ve oturum açar, satır tüketilir. Ayrıca: token, yalnızca hashlenir ve düz metin olarak saklanmaz; süresi dolmuş bir kod reddedilir; mutlak bir sınır vardır; süper-admin, 403 ile engellenir; geçersiz bir token temiz bir 404 döner verilen bir 500 yerine; doğrulama hem web oturumu altında hem de Bearer token ile çalışır; ve prune komutu doğru satırları temizler. Tam çekirdek test seti 497 test yeşil döner.
En çok gurur duyduğum şey, beni yakalayan kısım
En çok gurur duyduğum şey, beni yakalayan kısım
İşte bu, işin kamuya açık tarafı. Bu çalışmanın her alt aşaması, birleştirilmeden önce kırmaya çalışan iki zıt gözden geçiren tarafından test edildi. QR arka ucunda, bir yüksek seviyede sorun tespit ettiler: doğrulama URL’si token’i taşıyor ve etkinlik günlüğü tam istek URL’sini kaydediyordu. Bu nedenle, dikkatle hash’lediğim token, denetim kaydında düz metin halinde duruyordu. Hashleme, günlükleme ile çözüldü.
Düzeltme, bir satıra yapılan bir yamanın ötesinde sistemik bir düzeltmeydi: etkinlik bağlamı, gizli yol segmentlerini (rota parametre adı üzerinden) sorgu dizesi gizli bilgileriyle aynı şekilde kapatır. Bir gözden geçiren, bir yüksek seviye kusur ve bunu kendi güvenimden daha iyi bir düzeltme sağladı. İnceleme kapısının var olma nedeni budur.
Takip edin
Takip edin
Bu, Gerçek, üretim kodundan kamuya açık olarak çıkardığım Laravel’e özgü yeniden kullanılabilir bir SaaS/CRM çekirdeği olan LaraFoundry’nin bir parçasıdır.
Guard-agnostic doğrulama son noktası (bir denetleyicinin, bugün bir web oturumunu ve yarın bir mobil Bearer token’ı hizmet vermesi için) kendi kısa yazımını hak ediyor. Bu, serinin bir sonraki yazısıdır.
Kaynak: Orijinal Makale
- Akış, bir paragrafta
- On bir
- Açık 1: token düz metin olarak veri tabanında
- Açık 3: tarayıcı, çözülen şeyi fetch etti
- Açık 6: önce giriş yap, sonra sor
- Bağışlayanın asla sahip olmadığı şey: süper-admin engeli
- İçerideyken: sayfa veya modal
- Kanıt, hissiyat değil
- En çok gurur duyduğum şey, beni yakalayan kısım
- Takip edin


