← İçindekiler Güvenli Kodlama EğitimiKimlik Doğrulama ve Oturum Yönetimi
2009’da RockYou adlı sosyal medya sitesi 32 milyon kullanıcının parolasını sızdırdı. Parolalar düz metin olarak veritabanında duruyordu. Hiç şifreleme yoktu. Bu sızıntı, dünyanın en büyük parola sözlüğüne kaynak oldu: sızan parolalar, bugün hâlâ brute force saldırılarında kullanılıyor.
Özlü kural: Parola asla düz metin olarak saklanmaz. Hash’lenir, tuzlanır ve yavaş bir algoritmayla işlenir. “Veritabanım güvende” diye düşünme; veritabanı bir gün sızar, o gün elinde ne kalırsa ona bakacaksın.
Hızlı ipucu: Veritabanındaki parola kolonuna bak. Düz metin veya MD5 görüyorsan, bu gece düzelt.
53.1 Parola Saklama
Parola saklamanın tek doğru yolu vardır: yavaş, tuzlu, parametreli bir hash fonksiyonu kullanmak. Hızlı algoritmalar (MD5, SHA-1, SHA-256) parola saklamak için YANLIŞ’tır. Neden? Çünkü bu algoritmalar saniyede milyonlarca deneme yapabilir. Saldırgan bir GPU ile saniyede 100 milyar MD5 hash üretebilir. 8 karakterlik bir parola dakikalar içinde kırılır.
Kötü Yaklaşımlar
import hashlib
# YANLIS 1: Duz metin saklama
def save_password_bad_v1(password):
db.execute("INSERT INTO users (password) VALUES (?)", (password,))
# Veritabani sizarsa tum parolalar acikta
# YANLIS 2: MD5 (kirilmis, cok hizli)
def save_password_bad_v2(password):
hashed = hashlib.md5(password.encode()).hexdigest()
db.execute("INSERT INTO users (password) VALUES (?)", (hashed,))
# MD5 saniyede 100 milyar deneme yapilabilir
# Rainbow table ile aninda kirilir
# YANLIS 3: SHA-256 tuzsuz (hizli, rainbow table acigi)
def save_password_bad_v3(password):
hashed = hashlib.sha256(password.encode()).hexdigest()
db.execute("INSERT INTO users (password) VALUES (?)", (hashed,))
# SHA-256 saniyede milyarlarca deneme
# Tuz yok, ayni parola ayni hash uretir
Bu üç yöntem de yanlış çünkü hepsi HIZLI. Parola hash’lemek için hız istemezsin. Tam tersine, yavaş olmasını istersin. Saldırganın her denemesi 0.5 saniye sürerse, 8 karakterlik bir parolayı kırmak binlerce yıl alır.
Doğru Yaklaşım: bcrypt
import bcrypt
def hash_password(password: str) -> bytes:
"""Parolayi bcrypt ile guvenli sekilde hash'le"""
# 1. Parolayi bytes'a cevir
password_bytes = password.encode('utf-8')
# 2. bcrypt otomatik olarak tuz (salt) uretir
# gensalt(12) icindeki 12, maliyet faktorudur (cost factor)
# Her artis, hashleme suresini 2 katina cikarir
# 12 = yaklasik 0.3 saniye (varsayilan)
salt = bcrypt.gensalt(rounds=12)
# 3. Hash'le
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed
def verify_password(password: str, hashed: bytes) -> bool:
"""Kullanici girisini dogrula"""
password_bytes = password.encode('utf-8')
# checkpw, hash ve girisi karsilastirir
# timing attack'e karsi sabit sureli karsilastirma yapar
return bcrypt.checkpw(password_bytes, hashed)
# Kullanim
hashed_pw = hash_password("KullanicininParola123")
# Veritabanina hashed_pw kaydet
# Giris dogrulama
if verify_password(input_password, stored_hashed_pw):
print("Parola dogru")
else:
print("Parola yanlis")
bcrypt’in özelliği: hash’in içinde tuz (salt) gömülüdür. Aynı parola her seferinde farklı hash üretir. Maliyet faktörü (rounds=12) zamanla artırılabilir, böylece donanım geliştikçe güvenlik seviyesi korunur.
Alternatif: Argon2
# Argon2, bcrypt'in halefidir, daha modern bir parola hash algoritmasidir
# 2015 Parola Hashleme Yarismasi kazananidir
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # Iterasyon sayisi (yuksek = yavas = guvenli)
memory_cost=65536, # 64 MB bellek kullanimi (GPU saldirilarini engeller)
parallelism=4, # Es zamanli is parcacigi sayisi
)
def hash_password_argon2(password: str) -> str:
return ph.hash(password)
def verify_password_argon2(password: str, hash_str: str) -> bool:
try:
return ph.verify(hash_str, password)
except argon2.exceptions.VerifyMismatchError:
return False # Parola yanlis
except argon2.exceptions.InvalidHashError:
return False # Hash formati bozuk
Argon2, GPU ve ASIC saldırılarına karşı daha dirençli çünkü bellek gereksinimi yüksektir. Bir GPU 100 milyar MD5 hesaplayabilir ama sadece birkaç Argon2 hesaplayabilir çünkü her hesap 64 MB bellek ister.
Hangi algoritmayı kullanırsan kullan, kullanıcının parolasını ASLA loglama, ASLA ekrana yazma, ASLA hata mesajına ekleme. Parola hash’i bile loglanmamalı. Eğer bir hata ayıklama (debug) yapman gerekiyorsa, “parola doğru/yanlış” şeklinde çalış, parolanın kendisini asla yazdırma.
Pepper (Biber) kavramı: Salt her kullanıcı için farklıdır ve hash’in içinde saklanır. Pepper ise tüm kullanıcılar için aynıdır ve hash’in DIŞINDA, uygulamanın kodunda veya ayrı bir yapılandırmada saklanır. Saldırgan veritabanını çalsa bile pepper’ı bilemez, bu yüzden hash’leri kıramaz.
import os
import hmac
import hashlib
import bcrypt
# Pepper uygulamanin environment variable'inda saklanir
# Veritabaninda DEGIL
PEPPER = os.environ['PASSWORD_PEPPER'] # ornegin: 32 byte rastgele string
def hash_with_pepper(password: str) -> bytes:
"""Once pepper ile HMAC uygula, sonra bcrypt ile hash'le"""
# 1. Parola + pepper -> HMAC-SHA256
peppered = hmac.new(
PEPPER.encode('utf-8'),
password.encode('utf-8'),
hashlib.sha256
).hexdigest()
# 2. Pepper'lanmis degeri bcrypt ile hash'le
return bcrypt.hashpw(peppered.encode('utf-8'), bcrypt.gensalt(12))
def verify_with_pepper(password: str, hashed: bytes) -> bool:
peppered = hmac.new(
PEPPER.encode('utf-8'),
password.encode('utf-8'),
hashlib.sha256
).hexdigest()
return bcrypt.checkpw(peppered.encode('utf-8'), hashed)
- bcrypt (rounds=12+) veya Argon2 kullan
- Her kullanıcı için ayrı tuz (salt) kullan
- Pepper uygulama kodunda sakla, veritabanında değil
- Maliyet faktörünü yıllık gözden geçir, gerekirse artır
- MD5, SHA-1, SHA-256 ile parola hash'le
- Aynı tuzu tüm kullanıcılar için kullan
- Parolayı logla veya hata mesajına ekle
- Kendi hash fonksiyonunu yaz
53.2 Oturum Yönetimi
Kullanıcı giriş yaptı. Şimdi “bu kullanıcı hâlâ giriş yapmış mı?” sorusunu her istekte yanıtlaman gerek. Bunu oturum (session) yönetimi ile yaparsın.
Güvenli Oturum ID’si Üretimi
import secrets
def generate_session_id() -> str:
""" Kriptografik olarak guvenli oturum ID'si uret"""
# secrets module, random'dan farkli olarak
# isletim sisteminin entropy kaynagini kullanir
# 32 byte = 256 bit = 43 karakterlik URL-safe string
return secrets.token_urlsafe(32)
# KOTU: random veya uuid kullanma
import random
bad_id = str(random.randint(100000, 999999)) # Tahmin edilebilir!
import uuid
bad_id2 = str(uuid.uuid4()) # uuid4 guvenlidir ama secrets daha kisadir
Güvenli Cookie Ayarları
from flask import Flask, make_response
app = Flask(__name__)
@app.route('/login', methods=['POST'])
def login():
# ... parola dogrulama ...
session_id = generate_session_id()
save_session(session_id, user_id)
response = make_response("Giris basarili")
response.set_cookie(
'session_id',
session_id,
httponly=True, # JavaScript erisemez (XSS korumasi)
secure=True, # Sadece HTTPS uzerinden gonderilir
samesite='Lax', # CSRF korumasi
max_age=3600, # 1 saat sonra cookie biter
path='/', # Tum site gecerli
)
return response
| Cookie bayrağı | Ne yapar | Neden önemli |
|---|---|---|
HttpOnly | JavaScript cookie’ye erişemez | XSS ile cookie çalınmasını engeller |
Secure | Cookie sadece HTTPS’te gönderilir | Dinleme (sniffing) saldırılarını engeller |
SameSite=Lax | Baska siteden gelen istekte cookie gitmez | CSRF koruması sağlar |
max_age | Cookie’nin ömrü | Sonsuz oturum riskini azaltır |
Oturum timeout’ı iki türdür: mutlak (absolute) ve atıl (idle). Mutlak timeout, kullanıcı ne kadar aktif olursa olsun X saat sonra otomatik çıkar (örneğin bankacılıkta 15 dk). Atıl timeout, kullanıcı X dakika işlem yapmazsa çıkar. Kritik uygulamalarda ikisini birlikte kullan: bankacılıkta 15 dk atıl + 8 saat mutlak.
Oturum sabitleme (session fixation) saldırısı: Saldırgan, kullanıcıya kendi belirlediği bir oturum ID’si verir (örneğin URL ile). Kullanıcı bu ID ile giriş yapar. Saldırgan aynı ID’yi kullanarak kullanıcı hesabına girer.
def login_secure(user_id):
# ESKI oturum ID'sini hemen Invalidate et
old_session = request.cookies.get('session_id')
if old_session:
delete_session(old_session)
# HER ZAMAN yeni oturum ID'si uret
# Giris sirasinda oturum ID degismezse, fixation acigi var demektir
new_session_id = generate_session_id()
save_session(new_session_id, user_id)
return new_session_id
53.3 JWT Güvenliği
JWT (JSON Web Token), oturum bilgilerini JSON olarak taşıyan, dijital imzalı bir tokendır. Tarayıcıdan sunucuya her istekte gönderilir. Oturum bilgisi sunucuda değil, token’ın içinde taşınır (stateless).
JWT üç bölümden oluşur: header.payload.signature
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjMsImV4cCI6MTcwMA==.aBcDeFgHiJkL
|--- header ---||-------- payload --------||----- signature -----|
Doğru Kullanım
import jwt
import datetime
import os
SECRET_KEY = os.environ['JWT_SECRET'] # Cok uzun, rastgele bir anahtar
def create_jwt(user_id: int) -> str:
"""Giris yapmis kullanici icin JWT uret"""
payload = {
'user_id': user_id,
'iat': datetime.datetime.now(datetime.timezone.utc), # Olusturma zamani
'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=15),
'iss': 'myapp', # Token'i kim yayinladi
'aud': 'myapp-users', # Token kimin icin
}
# HS256 ile imzala
return jwt.encode(payload, SECRET_KEY, algorithm='HS256')
def verify_jwt(token: str) -> dict:
"""Token'i dogrula ve payload'u dondur"""
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=['HS256'], # SADECE beklenen algoritma
issuer='myapp',
audience='myapp-users',
)
return payload
except jwt.ExpiredSignatureError:
raise Exception("Token suresi dolmus")
except jwt.InvalidTokenError:
raise Exception("Gecersiz token")
Kritik Güvenlik Açıkları
1. alg: none açığı: En tehlikeli JWT açığı. Saldırgan token’ın header’ını {"alg": "none"} olarak değiştirir. Zayıf implementasyonlar imzayı kontrol etmeden token’ı kabul eder.
# KOTU KUTUPHANE: imzalamayi tamamen kapatir (alg=none dahil tum ataklara acik)
payload = jwt.decode(token, verify=False) # ASLA boyle yapma!
# IYI KUTUPHANE: sadece HS256 kabul eder
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
# algorithms parametresi ZORUNLU, eksik olamaz
2. Symmetric vs Asymmetric karışıklığı: Token RS256 (asymmetric) ile imzalanmış ama sunucu HS256 (symmetric) bekliyorsa, saldırgan public key’i kendi imzası olarak kullanabilir.
3. Hassas veri payload’da: JWT payload Base64 ile encode edilir, şifrelenmez. “password”, “credit_card” gibi verileri payload’a KOYMA. Token herkes tarafından okunabilir.
JWT iptal edilemez. Bir token yayınladığında, süresi dolana kadar geçerlidir. Kullanıcı “çıkış yap” dediğinde, tarayıcı token’ı siler ama token hâlâ geçerlidir. Çalındıysa saldırgan süresi dolana kadar kullanabilir. Çözüm: kısa süreli access token (15 dk) + uzun süreli refresh token (7 gün). Refresh token’ı veritabanında tut, iptal edebilirsin.
53.4 OAuth 2.0 ve OIDC
OAuth 2.0, bir uygulamanın başka bir uygulama adına hareket etmesine izin veren bir yetki protokolüdür. “Bu uygulamaya kendi adına tweet atma izni veriyor musun?” mantığı. OIDC (OpenID Connect), OAuth 2.0’ın üzerine inşa edilmiş bir kimlik doğrulama katmanıdır.
En yaygın akış: Authorization Code Flow + PKCE
# OAuth 2.0 Authorization Code Flow (kabaca)
# 1. Kullaniciyi yetki sunucusuna yonlendir
auth_url = (
f"https://auth.example.com/authorize?"
f"response_type=code"
f"&client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&scope=openid profile email"
f"&state={random_state}" # CSRF korumasi
f"&code_challenge={pkce_challenge}" # PKCE
)
# Tarayici auth_url'e yonlenir
# 2. Yetki sunucusu kod ile geri doner
# https://app.example.com/callback?code=xyz123&state=xxx
# 3. Kodu access token ile takas et
response = requests.post("https://auth.example.com/token", data={
"grant_type": "authorization_code",
"code": auth_code,
"client_id": CLIENT_ID,
"code_verifier": pkce_verifier, # PKCE
"redirect_uri": REDIRECT_URI,
})
access_token = response.json()["access_token"]
PKCE (Proof Key for Code Exchange): Mobil ve tek sayfa uygulamalarında client_secret güvenli saklanamaz. PKCE, her yetki isteği için dinamik bir secret üretir. 2024’ten beri tüm OAuth istemcilerinde PKCE zorunludur.
Implicit flow neden önerilmiyor?
Eski OAuth 2.0 uygulamalarında “implicit flow” kullanılırdı. Bu akışta access token doğrudan URL fragment’ında (#token=...) gönderilir. Sorun: token tarayıcı geçmişinde kalır, JavaScript’e sızar, loglanabilir. Modern uygulamalarda implicit flow yerine authorization code flow + PKCE kullanılır. Token URL’de değil, sunucudan sunucuya POST ile alınır.
53.5 MFA Mimarisi
Multi-Factor Authentication (MFA), kullanıcının kim olduğunu kanıtlamak için iki veya daha fazla faktör ister. Faktörler:
- Bildiği bir şey (parola, PIN)
- Sahip olduğu bir şey (telefon, hardware token)
- Olduğu bir şey (parmak izi, yüz tanıma)
TOTP (Time-based One-Time Password)
import pyotp
# 1. Kullanimci icin benzersiz secret uret
secret = pyotp.random_base32() # 32 karakter base32
# Bu secret'i veritabaninda kullanici ile birlikte sakla
# Kullaniciya QR kod olarak goster (Google Authenticator ile tara)
# 2. 6 haneli kod uret (30 saniyede bir degisir)
totp = pyotp.TOTP(secret)
current_code = totp.now() # ornegin: "123456"
# 3. Giris sirasinda kullanicinin girdigi kodu dogrula
user_code = request.form['mfa_code']
if totp.verify(user_code):
print("MFA dogrulandi")
else:
print("Yanlis kod")
# Burada rate limiting yap: 3 yanlistan sonra kilitle
Backup codes: Telefonunu kaybeden kullanıcı hesaba giremez. Bunun için kayıt sırasında tek kullanımlık yedek kodlar üret ve kullanıcıya yazdır:
import secrets
def generate_backup_codes(count=10) -> list:
"""Tek kullanimlik yedek kodlar uret"""
codes = []
for _ in range(count):
# Her kod 16 haneli, tire ile ayrik (ornegin: ABCD-1234-EFGH-5678)
raw = secrets.token_hex(8).upper()
formatted = '-'.join([raw[i:i+4] for i in range(0, 16, 4)])
codes.append(formatted)
return codes
MFA uygularken rate limiting şarttır. Saldırgan 6 haneli bir TOTP kodunu denemek için en fazla 1.000.000 kombinasyon dener. 30 saniyede sınırsız deneme yapılmasına izin verirsen, otomatik bir araç dakikalar içinde kodu bulur. Çözüm: 3 yanlış denemeden sonra hesabı 15 dakika kilitle veya denemeler arası 1 saniye bekleme süresi koy.
Bir kullanıcının JWT token’ı XSS açığı ile çalındı. Saldırgan bu token’ı kullanarak API’ye istek gönderiyor. JWT stateless olduğu için sunucu token’ın çalındığını bilemez. Token’ın süresi 1 saat. Bu 1 saat boyunca saldırgan istediği işlemi yapabilir. Çözüm: access token süresini kısa tut (15 dakika). Daha uzun süreli oturum için refresh token kullan ve refresh token’ı veritabanında tut. Çalınma şüphesi olduğunda refresh token’ı veritabanından sil, kullanıcı yeniden giriş yapmak zorunda kalsın.
SMS tabanlı MFA neden önerilmiyor?
SMS ile gönderilen doğrulama kodu, SIM swap saldırısına açıktır. Saldırgan telefon sağlayıcısını kandırıp kullanıcının numarasını kendi SIM kartına aktarır. Artık doğrulama SMS’leri saldırgana gider. Ayrıca SMS trafiği şifresiz protokollerle iletilebilir (SS7 açığı). NIST, SMS tabanlı MFA’yı “kısıtlı kullanım” kategorisine aldı. TOTP (Google Authenticator) veya hardware token (YubiKey) her zaman SMS’den daha güvenlidir.
- Parolalar bcrypt (rounds=12+) veya Argon2 ile hash’leniyor
- Her kullanıcı için ayrı salt + pepper kullanılıyor
- Oturum ID’leri secrets.token_urlsafe ile üretiliyor
- Cookie’lerde HttpOnly + Secure + SameSite bayrakları açık
- Girişte yeni oturum ID üretiliyor (fixation koruması)
- JWT’de algorithms parametresi zorunlu, alg:none kabul edilmiyor
- JWT payload’da hassas veri yok
- Access token kısa (15 dk), refresh token veritabanında
- MFA için TOTP kullanılıyor, rate limiting açık
- Backup codes üretiliyor ve tek kullanımlık
Gerçek olay: 2012’de LinkedIn, 165 milyon kullanıcının parolasını sızdırdı. Parolalar SHA-1 ile hash’lenmişti ama tuzsuz (unsalted). Saldırganlar, hazır tablolarla milyonlarca parolayı kırdı. Sonuç: parolalar darknet’te satıldı, kullanıcıların diğer hesapları da ele geçirildi. Ders: hash yetmez, tuz (salt) ve yavaş algoritma şarttır.
Benzetme: Kimlik doğrulama, bir binanın kapısındaki güvenlik görevlisidir. Parolanı söylersin, içeri alır. Oturum yönetimi, içeride dolaşırken “hâlâ sen misin?” diye biletini kontrol eden görevlidir. JWT ise kapıda biletini yırtıp cebine koyduğun sistemdir: içeride kimse seni tekrar kontrol etmez. Bilet çalınırsa, başkası senin yerine gezer.