OAuth 2.0 Güvenlik Açıkları ve Korunma Yöntemleri

Modern web uygulamalarının neredeyse tamamı artık OAuth 2.0 kullanıyor. Kullanıcı girişleri, üçüncü parti entegrasyonlar, mobil uygulamalar… Hepsi bu protokole dayanıyor. Peki ya güvenlik? OAuth 2.0 iyi tasarlanmış bir protokol olsa da, yanlış implementasyon edildiğinde saldırganlar için adeta kırmızı halı sermiş oluyorsunuz. Bu yazıda gerçek dünya senaryolarıyla OAuth 2.0 güvenlik açıklarını ve bunlara karşı alınabilecek önlemleri ele alacağım.

OAuth 2.0 Nedir ve Neden Önemli?

OAuth 2.0, yetkilendirme için kullanılan açık bir standarttır. Kimlik doğrulama (authentication) ile yetkilendirme (authorization) kavramlarını birbirinden ayırt etmek burada kritik önem taşıyor. OAuth 2.0 temelde yetkilendirme için tasarlanmıştır; kimlik doğrulama için OpenID Connect (OIDC) katmanı üzerine eklenir.

Bir sysadmin olarak şunu net söyleyeyim: OAuth akışlarını tam anlamadan production ortamına almak, yangın çıkmadan önce sigortaları sökmek gibidir. Sorun er ya da geç ortaya çıkar.

Temel bileşenler şunlardır:

  • Resource Owner: Kaynağa sahip olan kullanıcı
  • Client: Kaynaklara erişmek isteyen uygulama
  • Authorization Server: Token’ları veren sunucu
  • Resource Server: Korunan kaynakları barındıran sunucu

Yaygın OAuth 2.0 Güvenlik Açıkları

1. CSRF Saldırıları ve State Parametresi Eksikliği

OAuth akışında state parametresi, Cross-Site Request Forgery (CSRF) saldırılarına karşı korunmak için zorunludur. Bu parametreyi kullanmayan pek çok uygulama görüyorum ve bu gerçekten kritik bir hata.

Saldırı senaryosu şöyle işler: Saldırgan, kurbanın tarayıcısında kendi OAuth akışını başlatır. Kurban farkında olmadan saldırganın hesabına bağlanır. Bu sayede saldırgan kurbanın oturumunu ele geçirebilir.

# Zayıf implementasyon - state parametresi yok
curl -X GET "https://auth.example.com/oauth/authorize?
client_id=myapp&
redirect_uri=https://myapp.com/callback&
response_type=code"

# Güvenli implementasyon - state parametresi ile
STATE=$(openssl rand -hex 32)
echo "Generated state: $STATE"

curl -X GET "https://auth.example.com/oauth/authorize?
client_id=myapp&
redirect_uri=https://myapp.com/callback&
response_type=code&
state=$STATE"

State parametresini oluştururken kriptografik olarak güvenli rastgele değerler kullanmak zorunludur. Oturum bazlı olmalı ve tek kullanımlık olacak şekilde tasarlanmalıdır.

2. Redirect URI Manipülasyonu

Bu, en sık rastladığım ve en tehlikeli açıklardan biri. Authorization server’ın redirect URI doğrulamasındaki zayıflıklar, saldırganların authorization code’u çalmasına olanak tanır.

# Saldırgan, redirect_uri parametresini manipüle eder
# Orijinal URI: https://legitimate-app.com/callback
# Saldırgan URI: https://evil.com/steal

curl -X GET "https://auth.example.com/oauth/authorize?
client_id=myapp&
redirect_uri=https://evil.com/steal&
response_type=code&
state=abc123"

# Nginx ile redirect URI whitelist kontrolü
# /etc/nginx/conf.d/oauth-proxy.conf
cat << 'EOF'
server {
    listen 443 ssl;
    
    location /oauth/authorize {
        # Redirect URI whitelist kontrolü
        if ($arg_redirect_uri !~ "^https://myapp.com/callback$") {
            return 400 "Invalid redirect_uri";
        }
        proxy_pass http://auth-server;
    }
}
EOF

Authorization server tarafında yapılması gereken kontroller:

  • Redirect URI’yı tam eşleşme (exact match) ile doğrulayın
  • Wildcard kullanımından kaçının
  • Kayıt sırasında izin verilen URI’ları önceden tanımlayın
  • Path traversal karakterlerini (/../) filtreleyin

3. Authorization Code Interception

PKCE (Proof Key for Code Exchange) kullanmayan uygulamalar, authorization code interception saldırılarına açıktır. Bu özellikle mobil uygulamalar için kritik bir sorundur.

# PKCE implementasyonu - code_verifier ve code_challenge oluşturma
# Code Verifier: 43-128 karakter arası kriptografik rastgele string

CODE_VERIFIER=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9-._~' | head -c 64)
echo "Code Verifier: $CODE_VERIFIER"

# Code Challenge: code_verifier'ın SHA256 hash'inin Base64URL encoding'i
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr -d '=' | tr '+/' '-_')
echo "Code Challenge: $CODE_CHALLENGE"

# Authorization isteği - PKCE ile
curl -X GET "https://auth.example.com/oauth/authorize?
client_id=myapp&
redirect_uri=https://myapp.com/callback&
response_type=code&
code_challenge=$CODE_CHALLENGE&
code_challenge_method=S256&
state=$(openssl rand -hex 32)"
# Token exchange sırasında code_verifier gönderme
AUTH_CODE="received_authorization_code_here"

curl -X POST "https://auth.example.com/oauth/token" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -d "grant_type=authorization_code" 
  -d "code=$AUTH_CODE" 
  -d "redirect_uri=https://myapp.com/callback" 
  -d "client_id=myapp" 
  -d "code_verifier=$CODE_VERIFIER"

4. Token Sızdırma ve Güvensiz Depolama

Token’ların nerede ve nasıl saklandığı kritik öneme sahiptir. LocalStorage’da saklanan access token’lar XSS saldırılarına karşı tamamen savunmasızdır.

# Redis ile güvenli token yönetimi
# Token'ları server-side session'da saklama örneği

# Redis'e token kaydetme (TTL ile)
redis-cli SET "session:abc123:access_token" "eyJhbGc..." EX 3600
redis-cli SET "session:abc123:refresh_token" "dGhpcyBp..." EX 86400

# Token doğrulama scripti
cat << 'SCRIPT'
#!/bin/bash
SESSION_ID=$1
TOKEN=$(redis-cli GET "session:${SESSION_ID}:access_token")

if [ -z "$TOKEN" ]; then
    echo "Token bulunamadi veya suresi doldu"
    exit 1
fi

# Token introspection endpoint'i ile doğrulama
RESPONSE=$(curl -s -X POST "https://auth.example.com/oauth/introspect" 
  -H "Authorization: Basic $(echo -n 'client_id:client_secret' | base64)" 
  -d "token=$TOKEN")

ACTIVE=$(echo $RESPONSE | jq -r '.active')

if [ "$ACTIVE" = "true" ]; then
    echo "Token gecerli"
    exit 0
else
    echo "Token gecersiz"
    redis-cli DEL "session:${SESSION_ID}:access_token"
    exit 1
fi
SCRIPT

5. Implicit Flow Kullanımı

Implicit flow artık deprecated durumdadır ve kullanılmamalıdır. Token’ların URL fragment’ında döndürülmesi, browser history ve log’larda token sızdırılmasına yol açar.

# YANLIS - Implicit flow (kullanmayin)
curl -X GET "https://auth.example.com/oauth/authorize?
response_type=token&
client_id=myapp&
redirect_uri=https://myapp.com/callback"
# Bu akışta token URL'de görünür: https://myapp.com/callback#access_token=TOKEN

# DOGRU - Authorization Code + PKCE kullanin
curl -X GET "https://auth.example.com/oauth/authorize?
response_type=code&
client_id=myapp&
redirect_uri=https://myapp.com/callback&
code_challenge=$CODE_CHALLENGE&
code_challenge_method=S256&
state=$STATE"

Token Güvenliği ve Yönetimi

Access Token Ömrünü Kısaltın

Uzun ömürlü access token’lar, sızdırıldıklarında uzun süre kötüye kullanılabilir. Genel kural olarak access token’lar kısa ömürlü, refresh token’lar ise uzun ömürlü olmalıdır.

# Keycloak üzerinden token süre yapılandırması
# keycloak-config.json

cat << 'EOF'
{
  "accessTokenLifespan": 300,
  "accessTokenLifespanForImplicitFlow": 900,
  "ssoSessionIdleTimeout": 1800,
  "ssoSessionMaxLifespan": 36000,
  "offlineSessionIdleTimeout": 2592000,
  "clientSessionIdleTimeout": 0,
  "clientSessionMaxLifespan": 0
}
EOF

# Keycloak realm konfigürasyonu uygulama
curl -X PUT "https://keycloak.example.com/admin/realms/myrealm" 
  -H "Authorization: Bearer $ADMIN_TOKEN" 
  -H "Content-Type: application/json" 
  -d @keycloak-config.json

Refresh Token Rotation

Refresh token rotation, bir refresh token kullanıldığında yenisinin verilmesi ve eskisinin geçersiz kılınması pratiğidir. Bu sayede token çalınması durumunda saldırgan yakalanabilir.

#!/bin/bash
# Refresh token rotation scripti

REFRESH_TOKEN=$1
CLIENT_ID="myapp"
CLIENT_SECRET="mysecret"

# Yeni token almak için refresh token kullan
RESPONSE=$(curl -s -X POST "https://auth.example.com/oauth/token" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -d "grant_type=refresh_token" 
  -d "refresh_token=$REFRESH_TOKEN" 
  -d "client_id=$CLIENT_ID" 
  -d "client_secret=$CLIENT_SECRET")

# Yanıtı parse et
NEW_ACCESS_TOKEN=$(echo $RESPONSE | jq -r '.access_token')
NEW_REFRESH_TOKEN=$(echo $RESPONSE | jq -r '.refresh_token')
ERROR=$(echo $RESPONSE | jq -r '.error')

if [ "$ERROR" != "null" ]; then
    echo "Token yenileme basarisiz: $ERROR"
    # Kullanıcıyı logout yap
    redis-cli DEL "session:$SESSION_ID:*"
    exit 1
fi

# Eski refresh token'ı geçersiz kıl ve yenisini kaydet
redis-cli SET "session:$SESSION_ID:access_token" "$NEW_ACCESS_TOKEN" EX 300
redis-cli SET "session:$SESSION_ID:refresh_token" "$NEW_REFRESH_TOKEN" EX 86400

echo "Token basariyla yenilendi"

Token Blacklisting

Kullanıcı logout olduğunda veya şüpheli aktivite tespit edildiğinde token’ları geçersiz kılmak gerekir.

#!/bin/bash
# Token blacklist yönetimi

TOKEN=$1
ACTION=$2  # "blacklist" veya "check"

# Token'ın JTI (JWT ID) değerini al
JTI=$(echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq -r '.jti')
EXP=$(echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq -r '.exp')

if [ "$ACTION" = "blacklist" ]; then
    NOW=$(date +%s)
    TTL=$((EXP - NOW))
    
    if [ $TTL -gt 0 ]; then
        redis-cli SET "blacklist:$JTI" "1" EX $TTL
        echo "Token blacklist'e eklendi. TTL: $TTL saniye"
    else
        echo "Token zaten suresi dolmus"
    fi
    
elif [ "$ACTION" = "check" ]; then
    BLACKLISTED=$(redis-cli EXISTS "blacklist:$JTI")
    
    if [ "$BLACKLISTED" = "1" ]; then
        echo "Token blacklist'te - erisim reddedildi"
        exit 1
    else
        echo "Token temiz"
        exit 0
    fi
fi

Gerçek Dünya Senaryo: API Gateway’de OAuth Güvenliği

Kurumsal bir ortamda API gateway katmanında OAuth güvenliğini nasıl uygularsınız? İşte pratik bir yaklaşım:

# Kong API Gateway ile OAuth 2.0 plugin yapılandırması
# plugin-config.json

curl -X POST "http://kong-admin:8001/services/my-api/plugins" 
  -H "Content-Type: application/json" 
  -d '{
    "name": "oauth2",
    "config": {
      "scopes": ["read", "write", "admin"],
      "mandatory_scope": true,
      "token_expiration": 300,
      "enable_authorization_code": true,
      "enable_client_credentials": true,
      "enable_implicit_grant": false,
      "enable_password_grant": false,
      "accept_http_if_already_terminated": false,
      "anonymous": null,
      "global_credentials": false,
      "auth_header_name": "Authorization",
      "hide_credentials": true,
      "provision_key": "'$(openssl rand -hex 32)'"
    }
  }'

# Rate limiting ekle - brute force koruması
curl -X POST "http://kong-admin:8001/services/my-api/plugins" 
  -H "Content-Type: application/json" 
  -d '{
    "name": "rate-limiting",
    "config": {
      "minute": 60,
      "hour": 1000,
      "policy": "redis",
      "redis_host": "redis",
      "redis_port": 6379
    }
  }'

Monitoring ve Anomali Tespiti

OAuth güvenliğinin sadece teknik implementasyondan ibaret olmadığını vurgulamak istiyorum. İzleme ve anomali tespiti en az implementasyon kadar önemlidir.

#!/bin/bash
# OAuth log analizi ve anomali tespiti scripti

LOG_FILE="/var/log/oauth-server/access.log"
ALERT_EMAIL="[email protected]"
THRESHOLD_FAILED=10  # 1 dakikada maksimum başarısız deneme

# Son 1 dakikadaki başarısız token isteklerini say
FAILED_COUNT=$(grep "$(date -d '1 minute ago' '+%Y-%m-%dT%H:%M')" "$LOG_FILE" | 
               grep "error" | 
               grep "invalid_grant|invalid_client|access_denied" | 
               wc -l)

if [ $FAILED_COUNT -gt $THRESHOLD_FAILED ]; then
    echo "UYARI: Son 1 dakikada $FAILED_COUNT basarisiz OAuth denemesi tespit edildi" | 
    mail -s "[GUVENLIK UYARISI] OAuth Brute Force Suphecisi" $ALERT_EMAIL
    
    # Şüpheli IP'leri topla ve geçici olarak engelle
    SUSPICIOUS_IPS=$(grep "$(date -d '1 minute ago' '+%Y-%m-%dT%H:%M')" "$LOG_FILE" | 
                    grep "invalid_grant" | 
                    awk '{print $1}' | 
                    sort | uniq -c | sort -rn | 
                    awk '$1 > 5 {print $2}')
    
    for IP in $SUSPICIOUS_IPS; do
        echo "Engelleniyor: $IP"
        iptables -A INPUT -s $IP -j DROP
        # 1 saat sonra kaldır
        at now + 1 hour << CMD
iptables -D INPUT -s $IP -j DROP
CMD
    done
fi

Scope Doğrulama ve En Az Yetki Prensibi

OAuth scope’ları, uygulamalara verilen izinleri sınırlandırmak için kullanılır. “Her şeye erişim” anlamına gelen geniş scope’lardan kaçınmak gerekir.

Pratikte sık gördüğüm yanlış uygulamalar:

  • Tüm API’lere erişim için tek bir api scope’u tanımlamak
  • Scope kontrolünü resource server yerine client tarafında yapmak
  • Scope’ları token içinde doğrulamamak

Doğru yaklaşım için scope hiyerarşisi şu şekilde tanımlanabilir:

  • read:users: Kullanıcı bilgilerini okuma
  • write:users: Kullanıcı bilgilerini güncelleme
  • delete:users: Kullanıcı silme
  • admin:users: Tüm kullanıcı işlemleri
  • read:orders: Sipariş okuma
  • write:orders: Sipariş oluşturma/güncelleme
# Resource server'da scope doğrulama
# validate-scope.sh

#!/bin/bash
TOKEN=$1
REQUIRED_SCOPE=$2

# Token'dan scope bilgisini al
TOKEN_SCOPES=$(echo $TOKEN | cut -d'.' -f2 | 
               base64 -d 2>/dev/null | 
               jq -r '.scope // ""')

# Gerekli scope'un mevcut olup olmadığını kontrol et
if echo "$TOKEN_SCOPES" | grep -qw "$REQUIRED_SCOPE"; then
    echo "Scope dogrulama basarili: $REQUIRED_SCOPE"
    exit 0
else
    echo "Yetersiz scope. Gerekli: $REQUIRED_SCOPE, Mevcut: $TOKEN_SCOPES"
    exit 1
fi

# Kullanım örneği:
# bash validate-scope.sh "$ACCESS_TOKEN" "write:users"

Client Credential Güvenliği

Server-to-server iletişimde kullanılan client credentials akışında, client_secret yönetimi kritik önem taşır.

Client secret yönetimi için dikkat edilmesi gerekenler:

  • Rotasyon: Client secret’ları düzenli olarak (90 günde bir) rotate edin
  • Depolama: Secret’ları asla source code’da tutmayın, vault kullanın
  • Erişim Kontrolü: Hangi servisin hangi secret’a eriştiğini kayıt altına alın
  • İzleme: Secret kullanımını loglayın ve anomalileri tespit edin
# HashiCorp Vault ile client secret yönetimi

# Secret kaydetme
vault kv put secret/oauth/myservice 
    client_id="myservice-client" 
    client_secret="$(openssl rand -hex 32)"

# Secret okuma ve kullanma
CLIENT_ID=$(vault kv get -field=client_id secret/oauth/myservice)
CLIENT_SECRET=$(vault kv get -field=client_secret secret/oauth/myservice)

# Client credentials token alma
TOKEN_RESPONSE=$(curl -s -X POST "https://auth.example.com/oauth/token" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  --cert /etc/ssl/myservice.crt 
  --key /etc/ssl/myservice.key 
  -d "grant_type=client_credentials" 
  -d "client_id=$CLIENT_ID" 
  -d "client_secret=$CLIENT_SECRET" 
  -d "scope=read:data")

ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.access_token')

# Token'ı kullan
curl -s "https://api.example.com/data" 
  -H "Authorization: Bearer $ACCESS_TOKEN"

TLS ve Transport Güvenliği

OAuth üzerinden yapılan tüm iletişim zorunlu olarak HTTPS üzerinden gerçekleşmelidir. Bu basit görünse de production ortamlarında atlanabilen bir detay.

Kontrol edilmesi gereken noktalar:

  • HSTS: HTTP Strict Transport Security başlığını etkinleştirin
  • TLS Versiyonu: TLS 1.2 minimum, tercihen TLS 1.3 kullanın
  • Sertifika Doğrulama: Client tarafında her zaman sertifika doğrulamasını açık tutun
  • Certificate Pinning: Yüksek güvenlikli uygulamalarda certificate pinning uygulayın
# Nginx OAuth proxy için TLS yapılandırması
cat << 'EOF' >> /etc/nginx/conf.d/oauth-security.conf

server {
    listen 443 ssl http2;
    server_name auth.example.com;
    
    ssl_certificate /etc/ssl/certs/auth.crt;
    ssl_certificate_key /etc/ssl/private/auth.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;
    ssl_stapling on;
    ssl_stapling_verify on;
    
    # HSTS - 1 yil, subdomainler dahil
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    
    # Guvenlik basliklarini ekle
    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;
    add_header Content-Security-Policy "default-src 'self'" always;
    
    location / {
        proxy_pass http://oauth-server:8080;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
    }
}
EOF

Güvenlik Denetimi Kontrol Listesi

OAuth 2.0 implementasyonunuzu düzenli olarak denetlemek için şu kontrolleri yapın:

  • State parametresi tüm authorization code akışlarında kullanılıyor mu?
  • PKCE tüm public client’larda (mobil, SPA) uygulanmış mı?
  • Redirect URI whitelist tam eşleşme ile mi doğrulanıyor?
  • Implicit flow devre dışı bırakılmış mı?
  • Access token ömrü 5-15 dakika arasında mı tutulmuş?
  • Refresh token rotation etkin mi?
  • Token’lar güvenli bir şekilde saklanıyor mu (HttpOnly cookie veya server-side)?
  • Token introspection endpoint’i korumalı mı?
  • Scope doğrulaması resource server’da yapılıyor mu?
  • Başarısız token isteklerine rate limiting uygulanmış mı?
  • TLS 1.2+ zorunlu kılınmış mı?
  • Client secret’lar vault’ta mı saklanıyor?
  • Token blacklist mekanizması mevcut mu?
  • Güvenlik logları merkezi olarak toplanıyor ve izleniyor mu?

Sonuç

OAuth 2.0 güçlü bir protokol ama gücünü doğru implementasyondan alıyor. Yanlış yapılandırılmış bir OAuth sistemi, güvenli sandığınız uygulamanızı tamamen açık hale getirebilir.

En önemli pratik çıkarımlar şunlardır: PKCE kullanın, implicit flow’u unutun, redirect URI’larınızı sıkı tutun, token ömürlerini kısaltın, her şeyi loglayın ve izleyin. Bu beş kural bile implementasyonunuzu ciddi ölçüde sağlamlaştırır.

Bir de şunu ekleyeyim: Güvenlik bir kez yapılan bir şey değil, sürekli devam eden bir süreçtir. OAuth implementasyonunuzu her büyük değişiklik öncesinde ve sonrasında gözden geçirin. Dependency’lerinizi takip edin, authorization library’lerinizi güncel tutun ve güvenlik bültenlerini düzenli okuyun.

Sormak istediğiniz bir şey varsa veya kurumunuza özgü bir OAuth senaryosunda takıldıysanız yorumlarda buluşalım.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir