İçeriğe atla
CodeAuditAgent
Tüm yazılar

JWT ve Oturum Güvenliğindeki Tuzaklar ve Bunlardan Kaçınma Yolları

Hesap ele geçirmeye yol açan JWT ve oturum hataları: algoritma karışıklığı, zayıf gizli anahtar, eksik claim kontrolü, token saklama, rotasyon ve gerçek çıkış.

· 7 dk okuma · Lina Source LLC

JSON Web Token'lar, uzun bir keskin kenar listesiyle gelen makul bir biçimdir. Sorun nadiren token'ın kendisidir. Hatalar; token'ın nasıl doğrulandığında, nerede saklandığında, ne kadar yaşadığında ve kullanıcı çıkış yaptığında ne olduğunda gizlidir. Bu hataların her birinin sonucu aynıdır: birinin elinde olmaması gereken bir token vardır ve sunucunuz onu kabul eder.

En sık karşılaşılan iki zayıflık, kriptografik imzanın hatalı doğrulanması anlamına gelen CWE-347 ile yetersiz oturum sonlandırma anlamına gelen CWE-613'tür. Aşağıdaki örnekler; Node.js'te, edge çalışma zamanlarında ve tarayıcılarda çalışan, JWT için yaygın kullanılan bir JavaScript kütüphanesi olan jose ile yazılmıştır.

Çözmek doğrulamak değildir

CWE-347'nin en doğrudan hâli, bir token'ın claim'lerini imzasını kontrol etmeden okumaktır. Her JWT kütüphanesinde hata ayıklama için bir decode fonksiyonu bulunur ve bu fonksiyon kimlik doğrulama ara katmanlarında olması gerekenden daha sık karşımıza çıkar. Çözülmüş bir token, herkesin yazabileceği bir base64 dizesinden ibarettir. JavaScript kod tabanlarında bu kalıp çoğu zaman token'ı noktalardan bölüp ortadaki parçaya JSON.parse uygulayan küçük bir yardımcı fonksiyonda saklanır. Test token'ları geçerli olduğu için her testte çalışır ve üretimde sahte üretilmiş her token'ı kabul eder.

import { decodeJwt, jwtVerify } from 'jose';

// Güvensiz: herkes sub alanı istediği kullanıcı ID'si olan bir token üretebilir
const claims = decodeJwt(token);
const userId = claims.sub;

// Doğru: önce imza, algoritma ve claim'ler kontrol edilir
const { payload } = await jwtVerify(token, key, { algorithms: ['HS256'] });
const verifiedUserId = payload.sub;

alg none ve algoritma karışıklığı

JWT başlığı, token'ı hangi algoritmanın imzaladığını söyler ve bu başlığı saldırgan kontrol eder. Ona güvenmekten iki klasik saldırı doğar. Birincisi alg değerinin none olmasıdır: bazı eski kütüphanelerin geçerli kabul ettiği, imzasız bir token. İkincisi algoritma karışıklığıdır: RS256 bekleyen ama algoritmayı başlığın seçmesine izin veren bir sunucuya, sunucunun genel anahtarı HMAC gizli anahtarı olarak kullanılarak imzalanmış bir HS256 token'ı verilebilir. Genel anahtar herkese açık olduğundan saldırgan istediği her şeyi imzalayabilir.

Modern kütüphaneler her ikisine karşı da savunma yapar; jose, jwtVerify içinde güvenliksiz token'ları reddeder ve anahtar türünün algoritmayla eşleştiğini kontrol eder. Yalnızca varsayılanlara güvenmeyin. Algoritma listesini her doğrulama çağrısında açıkça sabitleyin; böylece ileride yapılacak bir refactor ya da kütüphane değişimi listeyi sessizce genişletemez. Örneğin bir anahtar geçişi sırasında birden fazla algoritmayı kabul ediyorsanız, ikisini de açıkça listeleyin ve her biri için ayrı bir anahtar kullanın. Anahtarları rotasyona soktuğunuzda başlığa bir kid koyun ve anahtarı, token başlığında belirtilen bir URL'den değil, kendi listenizden kid ile seçin.

Zayıf imzalama anahtarları

Bir HS256 token'ı paylaşılan bir gizli anahtarla imzalanır. Gizli anahtar kısaysa ya da “secret”, uygulamanın adı veya bir eğitim içeriğinden kopyalanmış bir değer gibi tahmin edilebilirse, elinde tek bir geçerli token bulunan bir saldırgan sıradan araçlarla anahtarı çevrimdışı olarak kaba kuvvetle bulabilir ve ardından istediği kullanıcı için token imzalayabilir. Çevrimdışı tahmin denemelerinde hız sınırı yoktur.

  • HS256 için en az 32 rastgele bayt kullanın; örneğin openssl rand -base64 32 ile üretin.
  • Gizli anahtarı ortam değişkenlerinden veya bir gizli anahtar yöneticisinden yükleyin; eksik ya da kısaysa uygulama açılışında hata verin.
  • Anahtarı asla commit etmeyin; bir kez commit edildiyse mutlaka değiştirin.
  • Birden fazla servis token doğrulayacak ama yalnızca biri token üretecekse, RS256 veya EdDSA gibi asimetrik bir algoritma kullanın; böylece doğrulayıcıların elinde yalnızca genel anahtar bulunur.

exp, aud ve iss doğrulaması

Geçerli bir imza yalnızca token'ı kimin ürettiğini kanıtlar. Token'ın size yönelik olup olmadığına ve hâlâ güncel olup olmadığına claim'ler karar verir. Süre bilgisi olmayan bir token sonsuza dek geçerlidir. Mobil API'niz için üretilmiş bir token, sırf ikisi de aynı kimlik sağlayıcısına güveniyor diye yönetim servisiniz tarafından kabul edilmemelidir. aud ve iss tam olarak bunun içindir.

import { SignJWT, jwtVerify } from 'jose';

const rawSecret = process.env.JWT_SECRET;
if (!rawSecret || rawSecret.length < 32) {
  throw new Error('JWT_SECRET must be set and at least 32 characters');
}
const secret = new TextEncoder().encode(rawSecret);

const ISSUER = 'https://api.example.com';
const AUDIENCE = 'https://app.example.com';

export function signAccessToken(userId: string, tokenVersion: number) {
  return new SignJWT({ tv: tokenVersion })
    .setProtectedHeader({ alg: 'HS256' })
    .setSubject(userId)
    .setIssuer(ISSUER)
    .setAudience(AUDIENCE)
    .setIssuedAt()
    .setExpirationTime('10m')
    .sign(secret);
}

export async function verifyAccessToken(token: string) {
  const { payload } = await jwtVerify(token, secret, {
    algorithms: ['HS256'],
    issuer: ISSUER,
    audience: AUDIENCE,
    requiredClaims: ['exp', 'sub'],
  });
  return payload;
}

jose, exp claim'i mevcut olduğunda onu kontrol eder; ancak exp içermeyen bir token aksi hâlde doğrulamayı geçerdi. requiredClaims seçeneği bu boşluğu kapatır. Harici bir kimlik sağlayıcısından gelen token'lar için doğrulamayı createRemoteJWKSet ile sağlayıcının yayımladığı anahtar kümesine karşı yapın ve yine de algoritmaları, issuer'ı ve audience'ı sabitleyin.

localStorage mı, httpOnly çerez mi

Token'ı localStorage içinde saklamak, onu sayfadaki her script tarafından okunabilir hâle getirir. Tek bir XSS açığı ya da ele geçirilmiş tek bir üçüncü taraf script'i, token'ın bir saldırgana gönderilip süresi dolana kadar her yerden kullanılması için yeterlidir.

httpOnly bir çerez JavaScript tarafından okunamaz. XSS yine de ciddidir; çünkü enjekte edilen script, sayfa açık olduğu sürece kullanıcı adına istek yapabilir. Ancak uzun ömürlü bir kimlik bilgisini çalıp sonradan tekrar kullanamaz. Kendi arka ucuyla konuşan tarayıcı uygulamalarında çerezler daha iyi bir varsayılandır. Ayrı bir API'yi çağıran tek sayfalık bir uygulamada ise erişim token'ını yalnızca bellekte, yenileme token'ını httpOnly bir çerezde tutmak makul bir orta yoldur.

// Express: güvenli özniteliklere sahip oturum çerezi
res.cookie('__Host-session', accessToken, {
  httpOnly: true, // JavaScript'ten okunamaz
  secure: true, // yalnızca HTTPS; __Host- ön eki bunu zorunlu kılar
  sameSite: 'lax', // siteler arası POST isteklerinde gönderilmez
  path: '/', // __Host- ön eki için zorunlu
  maxAge: 10 * 60 * 1000, // milisaniye, token ömrüyle aynı
});

__Host- ön eki tarayıcıya, çerez Secure değilse, path değeri / olarak ayarlanmamışsa veya bir Domain özniteliği taşıyorsa onu reddetmesini söyler; bu da ele geçirilmiş bir alt alan adının çerezin üzerine yazmasını engeller.

SameSite ve kapsamadıkları

SameSite=Lax, çerezi siteler arası POST isteklerinde ve alt kaynak yüklemelerinde engeller; bu da klasik CSRF saldırılarının çoğunu ortadan kaldırır. Yine de üst düzey GET gezinmelerinde çerezi gönderir, dolayısıyla durum değiştiren her GET endpoint'i açıkta kalır. Strict bunları da engeller, ancak kullanıcılar e-postadan veya başka bir siteden uygulamanıza gelen bir bağlantıyı izlediğinde oturumları kapanmış olur. None korumayı devre dışı bırakır ve Secure gerektirir.

Lax ile birlikte GET isteklerinin asla durum değiştirmemesi kuralı sağlam bir temeldir. Hassas değişiklikler için bir Origin header kontrolü ya da bir CSRF token'ı ekleyin. SameSite'in, kayıtlı alan adınızın tüm alt alan adlarını aynı site kabul ettiğini unutmayın; yani açığı olan bir alt alan adı yine de sahte istek üretebilir.

Rotasyon, iptal ve gerçek çıkış

Durum tutmayan bir JWT, süresi dolmadan iptal edilemez; her istekte veritabanına bakmamanın bedeli budur. CWE-613, bu ödünleşim göz ardı edildiğinde olanları tarif eder: erişimi gerçekten sonlandırmayan çıkış işlemleri, parola değişiklikleri ve hesap askıya almaları. Hesap silme, rol değiştirme ve birini ekipten çıkarma da birer iptal olayıdır ve her biri bir sonraki token süresi dolduğunda değil, bir sonraki istekte etkili olmalıdır.

  • Erişim token'larını kısa ömürlü tutun: günler değil, dakikalar mertebesinde.
  • Yenileme token'larını sunucu tarafında hash'lenmiş olarak saklayın ve her kullanımda rotasyona sokun.
  • Zaten rotasyona sokulmuş bir yenileme token'ı yeniden kullanılırsa bunu hırsızlık sayın ve token ailesinin tamamını iptal edin.
  • Kullanıcı satırına ve token'a bir token sürümü ekleyin; her yerden çıkış, parola değişikliği veya askıya alma durumunda bu sürümü artırın.
  • Oturum sabitleme saldırısını önlemek için girişte oturum kimliğini yeniden üretin.
export async function requireUser(token: string) {
  const payload = await verifyAccessToken(token);

  const user = await db.user.findUnique({
    where: { id: payload.sub },
    select: { id: true, tokenVersion: true, disabled: true },
  });

  // Artırılmış bir sürüm veya devre dışı bir hesap erişimi anında sonlandırır
  if (!user || user.disabled || user.tokenVersion !== payload.tv) {
    throw new Error('Session revoked');
  }
  return user;
}

Bu sorgu, istek başına bir veritabanı okumasını geri getirir; iptal edilebilirliğin dürüst bedeli budur. Birçok uygulama için çerezde opak bir oturum kimliği ve bir oturum tablosu hem daha basit hem daha güvenlidir. O zaman çıkış yapmak, bir satırı silmek demektir. JWT'leri bir eğitim içeriğinde varsayılan oldukları için değil, özelliklerinin gerçekten işinize yaradığı yerlerde, örneğin servisler arasındaki kısa ömürlü token'larda kullanın.

Hangisini seçerseniz seçin, çıkış işlemi sunucuda gerçekleşmelidir. Çerezi temizlemek veya token'ı tarayıcıdan silmek yalnızca kullanıcının kopyasını kaldırır; çalınmış bir kopya, sunucu onu reddedene kadar çalışmaya devam eder. Çıkışta sunucu tarafındaki oturumu veya yenileme token'ını silin, çerezi ayarlandığı ad, path ve özniteliklerle temizleyin ve kullanıcı her yerden çıkmayı seçtiyse token sürümünü artırın. Parola sıfırlama da diğer tüm oturumları sonlandırmalıdır.

Kısa bir inceleme kontrol listesi

  • Kimlik doğrulama yollarındaki decode çağrılarını arayın.
  • Her doğrulama çağrısının algoritmaları, issuer'ı ve audience'ı sabitlediğini ve exp'i zorunlu kıldığını teyit edin.
  • İmzalama anahtarının nasıl üretildiğini, yüklendiğini ve açılışta nasıl doğrulandığını kontrol edin.
  • Token'ların tarayıcıda nerede saklandığını bulun.
  • Çıkışın, parola değişikliğinin ve hesap askıya almanın mevcut oturumları sonlandırdığını test edin.

Bu kontroller büyük ölçüde kod yollarını baştan sona okumakla ilgilidir; CodeAuditAgent da bir denetime tam olarak böyle yaklaşır: bulgular alıntılanmış satır, CWE, istismar senaryosu ve bir yamayla birlikte gelir, böylece eksik bir audience kontrolü dakikalar içinde doğrulanıp düzeltilebilir.