JWT Güvenliği: Token Saldırıları ve Savunma
alg:none, zayıf secret, algoritma karışıklığı ve jwk/jku/kid header enjeksiyonu üzerinden imza doğrulama atlatma teknikleri ve katmanlı savunma
JWT saldırılarının neredeyse tamamı tek bir kök nedene dayanır: sunucu, imzayı doğrulayacağı algoritmayı ve anahtarı token'ın saldırgan kontrollü header'ından okur. Bu yazı alg:none atlatma, RS256→HS256 anahtar karışıklığı, zayıf secret brute-force ve jwk/jku/kid header enjeksiyonunu somut kod ve komut örnekleriyle çözümler; ardından algoritma allowlist'i, sabit güven çıpası ve kid sanitizasyonu ile katmanlı savunmayı gösterir.
JWT saldırı yüzeyi neden bu kadar geniş?
JSON Web Token (JWT) saldırılarının büyük çoğunluğu tek bir tasarım kusurundan doğar: sunucu, imzayı hangi algoritmayla ve hangi anahtarla doğrulayacağını token’ın kendi header bölümünden okur — ve bu header, tamamen saldırganın kontrolündeki, imzalanmadan gönderilen bir veri parçasıdır. Bir saldırgan alg alanını none yapıp imzayı silebilir, RS256‘yı HS256‘ya çevirip açık anahtarı HMAC secret’ı gibi kullanabilir, ya da jwk/jku/kid header parametreleriyle sunucuyu kendi kontrol ettiği anahtara yönlendirebilir. Sonuç her durumda aynıdır: geçerli imzaya sahip, keyfi iddialar (claims) taşıyan bir token üretmek ve admin olarak oturum açmak. Bu yazı bu atlatma tekniklerini kanıt seviyesinde çözümler ve her birine karşı doğrulanabilir savunmayı verir. Altın kural şudur: imzayı doğrulayan tarafın algoritma ve anahtar seçimi, token’dan değil, sunucunun kendi güvendiği yapılandırmasından gelmelidir.
JWT anatomisi: güven neyin üzerine kurulu?
Bir JWT üç Base64URL bölümden oluşur ve nokta ile birleşir: header.payload.signature. Header ve payload gizli değildir — herkes çözebilir; güvenliğin tamamı imzaya dayanır.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyIiwicm9sZSI6InVzZXIifQ.<imza>
header (alg, typ) payload (claims) signature
Header örneği ve saldırı yüzeyi:
{
"alg": "RS256", // ← imza algoritması: saldırgan bunu değiştirebilir
"typ": "JWT",
"kid": "key-2024", // ← anahtar seçici: dosya/DB lookup'a gider
"jku": "https://issuer.example/jwks.json" // ← anahtar seti URL'i
}
Kritik nokta: alg, kid, jku, jwk alanları imzanın parçası olsa da imzadan önce okunur. Sunucu, imzayı doğrulamak için önce hangi anahtarı/algoritmayı kullanacağına karar vermek zorundadır ve bu karar için elindeki tek girdi çoğu zaman header’ın kendisidir. İşte tüm sorun burada başlar: doğrulamayı yönlendiren metaveri, doğrulanmamış girdidir.
alg:none — imzayı tamamen atlatma
JWS spesifikasyonu, “imzasız” tokenlar için none algoritmasını tanımlar. Bu, meşru olarak imzanın başka bir katmanda korunduğu senaryolar içindir; ancak zayıf bir doğrulama mantığı alg değerini körü körüne dinlerse, saldırgan imzayı tamamen kaldırıp tokeni geçerli kabul ettirebilir.
Saldırı üç adımdır: payload’ı değiştir, alg‘yi none yap, imza bölümünü boşalt (nokta kalır, sonrası boştur):
# Header ve payload'ı manipüle et, imzayı sil
python3 - <<'PY'
import base64, json
def b64(d): return base64.urlsafe_b64encode(json.dumps(d,separators=(',',':')).encode()).rstrip(b'=').decode()
header = {"alg":"none","typ":"JWT"}
payload = {"sub":"admin","role":"admin"}
print(f"{b64(header)}.{b64(payload)}.") # ← sondaki nokta ve boş imza dikkat
PY
Üretilen token: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9.
Bu, JWT’nin yaygın olarak bilinen zafiyetlerinden biridir. CVE-2015-9235, Node.js jsonwebtoken kütüphanesini 4.2.2 sürümü öncesinde bir doğrulama atlatması (verification bypass) nedeniyle savunmasız bırakmıştır: kütüphane imza algoritmasını doğrudan token’ın header’ından okuyup ona güvendiği için, saldırgan asimetrik imzalı bir tokeni simetrik (HS*) algoritmaya çevirerek doğrulamayı geçebiliyordu. Düzeltme, anahtar tipine göre kabul edilen algoritmaları belirleyip header’daki alg‘yi bu allowlist’e karşı doğrulamayı ekledi — bu mekanizma hem alg:none‘ı hem de algoritma karışıklığını kapatır. Bazı none varyantlarını (büyük/küçük harf filtresini atlatmak için) None, nOnE, NONE gibi yazımlarla denemek klasik bir bypass tekniğidir.
Algoritma karışıklığı — RS256 → HS256 anahtar karışıklığı
Bu, JWT saldırılarının en zarif olanlarından biridir çünkü sistem gerçekten imza doğrular; sadece yanlış anahtar tipiyle doğrular. Senaryo şudur:
- Sunucu tokenları asimetrik
RS256ile imzalar: özel anahtarla imzalar, açık anahtarla doğrular. - Kütüphanenin genel
verify(token, key)fonksiyonu algoritmayı token’ınalgheader’ından okur. - Geliştirici, “bu fonksiyon sadece RS256 kullanacak” varsayımıyla fonksiyona RSA açık anahtarını verir.
Saldırgan alg‘yi HS256‘ya çevirir. Artık kütüphane simetrik HMAC-SHA256 çalıştırır ve doğrulama anahtarı olarak sunucuya verilen RSA açık anahtarını HMAC secret’ı gibi kullanır. HS256’nın tüm güvenliği secret’ın gizli olmasına dayanır — oysa RSA açık anahtarı tanımı gereği gizli değildir, herkese açıktır. Saldırgan aynı açık anahtarı HMAC secret’ı olarak kullanıp tokeni kendisi imzalayabilir.
# Saldırgan: sunucunun AÇIK anahtarını HMAC secret'ı gibi kullanarak imzalar
import jwt # PyJWT
with open("public.pem") as f:
public_key = f.read() # herkese açık, /jwks.json veya sertifikadan alınmış
forged = jwt.encode(
{"sub": "admin", "role": "admin"},
key=public_key, # ← RSA açık anahtar, HMAC secret olarak
algorithm="HS256", # ← simetriğe düşürülmüş
headers={"alg": "HS256"}
)
print(forged) # sunucu HS256 doğrularsa, açık anahtarla eşleşen imza geçerli sayılır
Açık anahtar dışarı verilmiyorsa ne olur? Saldırgan iki geçerli token’dan matematiksel olarak açık anahtarı geri türetebilir. PortSwigger’ın rsa_sign2n aracı (sig2n) tam olarak bunu yapar: iki farklı JWT’den birkaç aday açık anahtar (n modülüsü) üretir, doğru olanı sunucuya karşı denenerek bulunur.
# İki geçerli token'dan aday açık anahtarları türet
docker run --rm -it portswigger/sig2n <token1> <token2>
Kritik ayrıntı: forge işleminin başarılı olması için kullanılan açık anahtarın byte-byte, satır sonu ve PEM biçimi dahil sunucudaki anahtarla birebir aynı olması gerekir; bir newline farkı bile imzayı bozar.
Zayıf secret — HS256 brute-force
Sunucu doğru şekilde HS256 bekliyor ama secret zayıfsa, saldırgan tek bir geçerli token’ı offline kırarak secret’ı ele geçirir; sonrasında istediği tokeni imzalar. secret, password, admin, changeme, key123 gibi placeholder değerler ya da kopyalanmış kod örneklerindeki sabit anahtarlar en sık rastlanan hatalardan biridir.
HS256, HMAC’ı standalone bir string secret ile kullanır; bu secret sözlük saldırısına açıksa güvenlik çöker. hashcat‘in 16500 modu doğrudan JWT (HS256/384/512) kırmak içindir:
# Geçerli bir token'ı sözlükle kır (mode 16500 = JWT)
hashcat -a 0 -m 16500 token.txt /usr/share/wordlists/rockyou.txt
# Kırılan secret ile keyfi token imzala
Auth0’ın gösterdiği gibi, kısa veya düşük entropili secret’lar tüketici donanımında dahi kırılabilir. OWASP’ın somut gereksinimi nettir: secret kriptografik olarak güvenli bir üreteçle üretilmeli, en az 160 bit entropiye sahip olmalı ve en azından imza çıktısı kadar büyük olmalıdır (HS256 için 256 bit, HS384 için 384, HS512 için 512). Parolalar asla HMAC secret’ı olarak kullanılmamalıdır.
Header enjeksiyonu — jwk, jku ve kid manipülasyonu
Bu saldırı ailesi, JWS header’ının doğrulama anahtarını işaret edebilmesi veya taşıyabilmesi özelliğini istismar eder. Bir uygulama anahtarını bu alanlardan seçer ve sonucu önceden güvendiği bir çıpaya bağlamazsa, saldırgan sunucuyu kendi kontrol ettiği anahtara yönlendirir.
jwk enjeksiyonu — Header, doğrulama açık anahtarını gömülü olarak taşıyabilir. Zayıf sunucu bu gömülü anahtarla doğrularsa, saldırgan kendi anahtar çiftini üretir, açık anahtarı jwk‘ya koyar ve tokeni eşleşen özel anahtarla imzalar:
{
"alg": "RS256",
"typ": "JWT",
"jwk": { "kty":"RSA", "n":"<saldirgan_acik_anahtar_n>", "e":"AQAB" }
}
jku manipülasyonu — Header, anahtar setinin (JWK Set) URL’ini jku ile verir. Sunucu bu URL’i allowlist’e karşı doğrulamazsa, saldırgan jku‘yu kendi sunucusundaki bir JWK Set’e yöneltir. Bu aynı zamanda bir SSRF vektörüdür — URL sunucu tarafında fetch edildiği için iç ağa erişim ve açık yönlendirme (open redirect) zincirlemeleri mümkündür.
kid manipülasyonu — kid (Key ID), doğrulama anahtarını bir dosya sisteminden ya da veritabanından çekmek için kullanılan seçicidir. Değer sanitize edilmeden lookup’a gittiği için üç klasik enjeksiyon doğar:
// Path traversal: bilinen içerikli bir dosyayı "anahtar" olarak kullan
{ "alg":"HS256", "kid":"../../../../../../dev/null" }
kid /dev/null‘a işaret ederse okunan “anahtar” boş string olur; saldırgan tokeni boş secret ile HMAC imzalayarak doğrulamayı geçebilir. kid bir SQL sorgusuna giriyorsa SQL injection ile anahtarı saldırganın kontrol ettiği bir değere döndürmek, dosya yoluna giriyorsa /proc, log ya da yüklenmiş dosya gibi içeriği bilinen kaynaklara path traversal ve komut bağlamına giriyorsa komut enjeksiyonu mümkündür.
-- kid üzerinden SQLi: sabit, saldırganın bildiği bir anahtar döndür
kid = aaa' UNION SELECT 'attacker-known-secret'-- -
Saldırı × Mekanizma × Savunma matrisi
| Saldırı | Mekanizma (kök neden) | Somut istismar | Savunma |
|---|---|---|---|
| alg:none | Doğrulayıcı alg=none‘ı kabul edip imzayı atlar |
İmzayı sil, alg:none, claim değiştir |
none‘ı reddet; algoritma allowlist zorla (none hiçbir zaman listede olmasın) |
| RS256→HS256 karışıklığı | Genel verify() algoritmayı header’dan okur; açık anahtar HMAC secret sanılır |
Açık anahtarı HMAC secret’ı yaparak HS256 imzala; anahtar gizli değilse sig2n ile türet |
Beklenen algoritmayı kod tarafında sabitle; simetrik ve asimetrik algoritmaları asla karıştırma; anahtar tipi kontrolü |
| Zayıf secret brute-force | Düşük entropili/placeholder HMAC secret | hashcat -m 16500 ile offline kır |
≥256 bit, ≥160 bit entropi, CSPRNG ile üretilmiş; parola kullanma; rotasyon |
| jwk enjeksiyonu | Gömülü açık anahtarla doğrulama | Kendi açık anahtarını jwk‘ya koy, özel anahtarla imzala |
Header’dan anahtar alma; yalnızca out-of-band, güven çıpasına bağlı anahtar kullan |
| jku manipülasyonu | Anahtar seti URL’i doğrulanmadan fetch edilir | jku‘yu saldırgan sunucusuna yönelt (+ SSRF) |
Katı host allowlist; sabit jwks_uri; SSRF kontrolleri |
| kid enjeksiyonu | kid sanitize edilmeden dosya/DB/komut lookup’a gider |
Path traversal (/dev/null), SQLi, komut enjeksiyonu |
kid‘i sanitize/parametrize et; allowlist; dosya yolunu kanonikleştir |
Savunma: güvenli JWT doğrulama
1. Algoritmayı kod tarafında sabitle (allowlist). Doğrulayıcıya kabul ettiğin algoritmaları açıkça söyle; token’ın alg header’ına asla güvenme. OWASP’ın kuralı: “kabul edilen algoritmaları hardcode et ve açık anahtar imza algoritmalarıyla MAC algoritmalarını karıştırma.”
# PyJWT — doğru kullanım
import jwt
decoded = jwt.decode(
token,
public_key,
algorithms=["RS256"], # ← yalnızca beklenen algoritma; HS256'ya düşürme engellenir
options={"require": ["exp", "iss", "aud"]},
issuer="https://issuer.example",
audience="my-api",
)
// Node.js jsonwebtoken — doğru kullanım
const decoded = jwt.verify(token, publicKey, {
algorithms: ["RS256"], // ← header'daki alg bu listeye karşı doğrulanır
issuer: "https://issuer.example",
audience: "my-api",
});
2. Doğrulama anahtarını token’dan alma. OWASP net: “token içindeki anahtarı, ancak bir güven zinciriyle bir kök güven çıpasına bağlanabiliyorsa kullan.” Pratikte: jwk/jku/x5u header’larını yok say; anahtarı issuer’ın önceden yapılandırılmış jwks_uri metaverisinden ya da yerel keystore’dan al.
3. jku kullanılıyorsa katı host allowlist + SSRF savunması. URL’i izinli hostlar listesine karşı doğrula; jwks_uri‘yi issuer discovery’den sabitle; sunucu-taraflı fetch’i iç ağa/metadata servislerine erişemeyecek şekilde kısıtla.
4. kid’i injection vektörü olarak ele al. Lookup’tan önce sanitize et: allowlist ya da parametreli sorgu kullan, dosya yolunu kanonikleştirip bir kök dizine hapset.
import os, re
KEY_DIR = os.path.realpath("/etc/app/jwt-keys")
def load_key(kid: str) -> bytes:
if not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", kid): # katı allowlist deseni
raise ValueError("gecersiz kid")
path = os.path.realpath(os.path.join(KEY_DIR, kid + ".pem"))
if os.path.commonpath([KEY_DIR, path]) != KEY_DIR: # path traversal kapısı
raise ValueError("kid dizin disina cikiyor")
with open(path, "rb") as f:
return f.read()
5. Güçlü secret + rotasyon (HMAC kullanılıyorsa). CSPRNG ile en az 256 bit secret üret, ortam değişkeninde/gizli yönetiminde tut, koda gömme, düzenli döndür:
# 256-bit kriptografik secret üret
openssl rand -base64 32
6. Standart claim’leri her istekte doğrula. exp (süre), nbf, iss, aud zorunlu olsun; token ömrünü kısa tut; iptal için ham token yerine jti+iss tabanlı denylist kullan (OWASP, imza malleability’si nedeniyle ham token/hash’ini denylist anahtarı yapmayı güvensiz sayar).
Doğrulama ve test kontrol listesi
Bir JWT uygulamasını değerlendirirken sırayla şunları dene ve savunmayı doğrula:
alg‘yinone/None/NONEyap, imzayı sil → 200 dönerse zafiyet.RS256tokeniHS256‘ya çevir, açık anahtarı HMAC secret’ı yap; açık anahtar yoksasig2nile türet.- Geçerli bir HS256 token’ını
hashcat -m 16500ile sözlüğe karşı kır. jwk‘ya kendi açık anahtarını göm;jku‘yu kontrol ettiğin bir hosta yönelt.kid‘e../../../dev/null, SQLi payload’ları ve komut metakarakterleri enjekte et.- Savunma tarafında: doğrulayıcı çağrısında algoritma allowlist’i var mı, anahtar header’dan mı yoksa güvenli kaynaktan mı geliyor,
kidsanitize ediliyor mu, secret uzunluğu/entropisi yeterli mi — hepsini kod incelemesiyle teyit et.
Özetle JWT güvenliği tek bir ilkeye indirgenir: doğrulamanın “nasıl” ve “neyle” yapılacağı kararı token’dan değil, sunucunun kendi güvendiği yapılandırmasından gelmelidir. Algoritmayı sabitleyen, anahtarı güven çıpasına bağlayan, kid‘i sanitize eden ve secret’ı güçlü tutan bir uygulamada bu yazıdaki atlatmaların tamamı kapanır.
Sen de yaz, arşivde yerini al
AltaySec Arşiv'e katkı ver — uzmanlığını Türkçe siber güvenlik literatürüne kat.
Yazar Ol