Ders 16 / 23
Oturum Güvenliği
Girişte oturum kimliğinin yenilenmesi, oturum sabitlemenin gözlenebilir sonucu, eşzamanlı oturumların listelenip tek tek sonlandırılması, boşta kalma ile mutlak ömür sınırlarının birlikte uygulanması ve çerez niteliklerinin rolü.
İçindekiler
Önceki dersin son kuralı, başarılı bir sıfırlamanın hesaba ait tüm oturumları kapatmasıydı. O cümlenin karşılığı, oturumun sunucu tarafında bir kayıt olmasıdır: kapatılabilen şey, kaydı tutulan şeydir. Bu ders oturumun açılışından sonlandırılmasına kadar geçen ömrünü ele alıyor.
Oturum Tabanlı Kimlik Doğrulama dersinde oturumun ne olduğu kurulmuştu: sunucu bir kayıt tutar, kaydın kimliğini tarayıcıya çerezle verir, sonraki isteklerde o kimliği görünce kullanıcıyı tanır. Buradaki sorular kimliğin ne zaman değiştiği, ne kadar yaşadığı ve kimin kapatabildiğidir.
Oturum Sabitleme ve Girişte Yenileme
Kullanıcı siteye girmeden önce de bir oturuma sahip olabilir — dil tercihi, sepet, arama geçmişi gibi durumlar anonim bir oturumda tutulur. Kullanıcı giriş yaptığında bu oturumun ne olacağı bir karardır ve iki seçenek vardır: aynı kimlik girişli sayılır ya da kimlik yenilenir.
Birinci seçenek oturum sabitleme (session fixation) adı verilen soruna açıktır. Kullanıcının giriş öncesindeki oturum kimliğini bilen biri, kullanıcı giriş yaptığı anda aynı kimliğe sahip olmaya devam eder ve artık girişli bir oturumun kimliğini elinde tutar. Kimliğin nasıl öğrenildiği ayrı bir konudur; kurulumun kusuru, öğrenilmiş bir kimliğin giriş sonrasında da geçerli kalmasıdır.
Aşağıdaki blok iki kurulumu yerel bir sunucuda yan yana çalıştırıyor.
cat > oturum-sunucu.mjs <<'EOF' import { createServer } from "node:http"; import { randomBytes } from "node:crypto"; const oturumlar = new Map(); // kimlik -> { uye } const kimlikOku = (istek) => /(?:^|;\s*)oturum=([^;]+)/.exec(istek.headers.cookie ?? "")?.[1]; function yeniOturum(uye = null) { const kimlik = randomBytes(24).toString("base64url"); oturumlar.set(kimlik, { uye }); return kimlik; } const cerez = (kimlik) => `oturum=${kimlik}; Path=/; HttpOnly; SameSite=Lax`; createServer((istek, yanit) => { const yol = new URL(istek.url, "http://yerel").pathname; const kimlik = kimlikOku(istek); if (yol === "/saglik") return yanit.writeHead(200).end("hazir\n"); if (yol === "/") { // anonim ziyaret: oturum acilir if (kimlik && oturumlar.has(kimlik)) return yanit.writeHead(200).end(kimlik + "\n"); const yeni = yeniOturum(); return yanit.writeHead(200, { "set-cookie": cerez(yeni) }).end(yeni + "\n"); } if (yol === "/kusurlu/giris") { // reddedilmesi gereken kurulum const o = oturumlar.get(kimlik); if (!o) return yanit.writeHead(400).end("oturum yok\n"); o.uye = "U-1001"; // ayni kimlik, artik girisli return yanit.writeHead(200).end(kimlik + "\n"); } if (yol === "/giris") { // dogru kurulum if (kimlik) oturumlar.delete(kimlik); // eski kimlik gecersizlesir const yeni = yeniOturum("U-1001"); return yanit.writeHead(200, { "set-cookie": cerez(yeni) }).end(yeni + "\n"); } if (yol === "/ben") { const o = oturumlar.get(kimlik); return yanit.writeHead(o ? 200 : 401).end((o ? (o.uye ?? "anonim") : "gecersiz") + "\n"); } yanit.writeHead(404).end(""); }).listen(8495, "127.0.0.1"); EOF node oturum-sunucu.mjs & sunucu=$! until curl -sf http://127.0.0.1:8495/saglik > /dev/null; do :; done dene() { kavanoz=$(mktemp); yol=$1 once=$(curl -s -c "$kavanoz" -b "$kavanoz" http://127.0.0.1:8495/) sonra=$(curl -s -c "$kavanoz" -b "$kavanoz" "http://127.0.0.1:8495$yol") echo " giris oncesi kimlik : ${once:0:14}..." echo " giris sonrasi kimlik: ${sonra:0:14}..." echo " kimlik degisti mi : $([ "$once" = "$sonra" ] && echo hayir || echo evet)" echo " eski kimlikle /ben : $(curl -s -H "cookie: oturum=$once" http://127.0.0.1:8495/ben)" rm -f "$kavanoz" } echo "--- reddedilmesi gereken kurulum (/kusurlu/giris) ---"; dene /kusurlu/giris echo "--- dogru kurulum (/giris) ---"; dene /giris kill $sunucu rm -f oturum-sunucu.mjs
--- reddedilmesi gereken kurulum (/kusurlu/giris) --- giris oncesi kimlik : BpWfAGGoOo1Zvs... giris sonrasi kimlik: BpWfAGGoOo1Zvs... kimlik degisti mi : hayir eski kimlikle /ben : U-1001 --- dogru kurulum (/giris) --- giris oncesi kimlik : qmM42rssGO2qEm... giris sonrasi kimlik: w1CarutBqemvD1... kimlik degisti mi : evet eski kimlikle /ben : gecersiz
Kimlik değerleri her çalıştırmada değişir; iki kurulumun farkı değişmez. Reddedilmesi
gereken kurulumda giriş öncesinde ele geçmiş bir kimlik, giriş sonrasında U-1001 olarak
yanıt alıyor. Doğru kurulumda aynı kimlik gecersiz dönüyor, çünkü giriş anında eski
kayıt silinip yenisi açılıyor.
Kuralın yazılışı tek cümledir: yetki düzeyi değiştiren her işlemde oturum kimliği yenilenir. Giriş bunun en belirgin örneğidir; parola değişikliği, ikinci etmenin tamamlanması ve rol yükselmesi de aynı kuralın kapsamındadır. Yenileme sırasında anonim oturumdaki taşınması gereken veriler yeni kayda kopyalanır — dil tercihi gibi. Taşınmayan şey kimliğin kendisidir.
Eşzamanlı Oturumlar
Bir üye aynı anda birden çok yerden girmiş olabilir: şubedeki masaüstünden, telefonundan, salon terminalinden. Bunların her biri ayrı bir oturum kaydıdır ve ayrı ayrı sonlandırılabilir olmalıdır. Kullanıcıya gösterilen “açık oturumlar” listesi, hesabında beklenmedik bir erişim olup olmadığını görebildiği tek yerdir.
Kayıt, oturum kimliğinin kendisini değil karmasını tutar. Gerekçe önceki dersteki sıfırlama belirteciyle aynıdır: veritabanını okuyabilen biri, açık oturumları kullanabilecek durumda olmamalıdır.
cat > oturum-depo.mjs <<'EOF' import { randomBytes, createHash } from "node:crypto"; import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync(":memory:"); db.exec(`CREATE TABLE oturum( karma TEXT PRIMARY KEY, uye TEXT NOT NULL, aygit TEXT NOT NULL, acilis INTEGER NOT NULL, son_erisim INTEGER NOT NULL)`); const karmala = (k) => createHash("sha256").update(k).digest("hex"); const BOSTA_MS = 30 * 60 * 1000; // bosta kalma siniri: 30 dakika const MUTLAK_MS = 12 * 60 * 60 * 1000; // mutlak omur: 12 saat const T0 = Date.parse("2026-03-10T09:00:00Z"); // ornegin "simdi"si function ac(uye, aygit, acilisFark, erisimFark) { const kimlik = randomBytes(24).toString("base64url"); db.prepare("INSERT INTO oturum VALUES (?,?,?,?,?)") .run(karmala(kimlik), uye, aygit, T0 - acilisFark, T0 - erisimFark); return kimlik; } const dk = 60 * 1000, sa = 60 * dk; ac("U-1001", "masaustu-sube-1", 2 * sa, 3 * dk); const telefon = ac("U-1001", "telefon", 1 * sa, 45 * dk); const eski = ac("U-1001", "salon-terminali", 14 * sa, 5 * dk); ac("U-1002", "telefon", 30 * dk, 1 * dk); const durum = `CASE WHEN ? - son_erisim > ${BOSTA_MS} THEN 'bosta doldu' WHEN ? - acilis > ${MUTLAK_MS} THEN 'mutlak omur doldu' ELSE 'gecerli' END`; const listele = (uye) => db.prepare( `SELECT aygit, (? - acilis)/60000 AS acilis_dk, (? - son_erisim)/60000 AS bosta_dk, ${durum} AS durum FROM oturum WHERE uye = ? ORDER BY acilis`).all(T0, T0, T0, T0, uye); const yaz = (baslik, uye) => { console.log(baslik); for (const s of listele(uye)) { console.log(` ${s.aygit.padEnd(16)} acilis=${String(s.acilis_dk).padStart(3)} dk ` + `bosta=${String(s.bosta_dk).padStart(2)} dk ${s.durum}`); } }; yaz("U-1001 oturumlari:", "U-1001"); const silinen = db.prepare("DELETE FROM oturum WHERE karma = ? AND uye = ?") .run(karmala(eski), "U-1001").changes; console.log(`\ntek oturum sonlandirildi (etkilenen satir: ${silinen})`); yaz("U-1001 oturumlari:", "U-1001"); const hepsi = db.prepare("DELETE FROM oturum WHERE uye = ?").run("U-1001").changes; console.log(`\nsifirlama sonrasi tum oturumlar kapatildi (etkilenen satir: ${hepsi})`); console.log("U-1001 kalan oturum :", db.prepare("SELECT COUNT(*) s FROM oturum WHERE uye='U-1001'").get().s); console.log("U-1002 kalan oturum :", db.prepare("SELECT COUNT(*) s FROM oturum WHERE uye='U-1002'").get().s); console.log("telefon kimligi hala gecerli mi:", db.prepare("SELECT COUNT(*) s FROM oturum WHERE karma = ?").get(karmala(telefon)).s > 0); EOF node oturum-depo.mjs
U-1001 oturumlari: salon-terminali acilis=840 dk bosta= 5 dk mutlak omur doldu masaustu-sube-1 acilis=120 dk bosta= 3 dk gecerli telefon acilis= 60 dk bosta=45 dk bosta doldu tek oturum sonlandirildi (etkilenen satir: 1) U-1001 oturumlari: masaustu-sube-1 acilis=120 dk bosta= 3 dk gecerli telefon acilis= 60 dk bosta=45 dk bosta doldu sifirlama sonrasi tum oturumlar kapatildi (etkilenen satir: 2) U-1001 kalan oturum : 0 U-1002 kalan oturum : 1 telefon kimligi hala gecerli mi: false
Silme deyimindeki AND uye = ? koşulu, sonraki konunun tamamının habercisidir: kullanıcı
yalnız kendi oturumunu kapatabilmelidir. Koşul olmadan, kimliği bilen herkes herhangi bir
oturumu kapatabilir. Bu, nesne düzeyinde erişim denetiminin bu dersteki ilk görünümüdür.
Son üç satır sıfırlama kuralının kanıtıdır: U-1001’in tüm oturumları kapandı, U-1002’nin oturumu etkilenmedi ve daha önce elde tutulan telefon kimliği artık kayıtta yok.
İki Ömür Sınırı
Çıktının durum sütunu iki ayrı sınırı birlikte değerlendiriyor.
Boşta kalma sınırı, son erişimden bu yana geçen süreyi ölçer. Amacı, kullanıcının başında olmadığı bir aygıtta oturumun açık kalmasını engellemektir. Telefon oturumu bu sınıra takıldı: kırk beş dakikadır kullanılmıyor.
Mutlak ömür, oturumun açılışından bu yana geçen süreyi ölçer ve kullanım ne olursa olsun uygulanır. Amacı, sürekli kullanılan bir oturumun süresiz yaşamasını engellemektir. Salon terminalindeki oturum bu sınıra takıldı: beş dakika önce kullanılmış olmasına karşın on dört saattir açık.
İkisi birlikte bulunur çünkü ayrı sorunları çözer. Yalnız boşta kalma sınırı olan bir kurulumda, ele geçirilmiş bir oturum düzenli isteklerle sonsuza kadar tazelenebilir. Yalnız mutlak ömür olan bir kurulumda, terk edilmiş bir aygıttaki oturum sınıra kadar açık kalır.
Değerlerin seçimi hesabın yetkisine göre değişir. Ödünç masasındaki personel oturumu için kısa boşta kalma sınırı, üye oturumu için daha uzun bir sınır seçilebilir. Ölçüt, oturumun açabildiği kapının genişliğidir.
Sınırların uygulanması bir sorguda yapılır; ayrı bir temizleme görevi ise süresi dolmuş kayıtları siler. İkisi birbirinin yerine geçmez: temizleme görevi gecikirse sorgu yine doğru karar verir, sorgu gecikmeli çalışırsa temizleme kaydı yine de kaldırır.
Kimliğin Taşınması
Oturum kimliği tarayıcıyla sunucu arasında çerezle taşınır ve çerezin nitelikleri kimliğin korunmasının bir parçasıdır. Yukarıdaki sunucu üç nitelik yazıyor.
HttpOnly, çerezin sayfadaki betiklerden okunmasını engeller. İstemci Güvenliği konusunda
ele alınan siteler arası betik çalıştırma bir sayfada gerçekleştiğinde, bu nitelik oturum
kimliğinin doğrudan okunmasının önüne geçer.
SameSite, çerezin başka sitelerden başlatılan isteklere eklenmesini sınırlar ve siteler
arası istek sahteciliğine karşı ilk savunmadır.
Secure niteliği yukarıdaki örnekte yoktur, çünkü yerel sunucu düz HTTP konuşuyor. Gerçek
bir dağıtımda bu nitelik zorunludur: çerez yalnız güvenli taşıma üzerinden gönderilir.
Taşıma güvenliği olmadan diğer iki niteliğin değeri sınırlıdır — kimlik ağ üzerinde açık
gider.
Bu üç nitelik, kimliğin başkasının eline geçmesini — oturumun ele geçirilmesini (session hijacking) — zorlaştırır ama tümüyle engellemez. Bu yüzden sunucu tarafında ikinci bir katman bulunur: oturum kaydında aygıt ve kaynak bilgisi tutulur, beklenmedik bir değişimde oturum sonlandırılıp yeniden kimlik doğrulama istenir. Bu denetimin sıkılığı bir ödünleşimdir; ağ değiştiren bir kullanıcıyı sürekli dışarı atan bir kural, kullanıcıyı oturumu hiç kapatmamaya iter.
Özet
- Yetki düzeyi değiştiren her işlemde oturum kimliği yenilenir; yenilenmeyen kurulumda giriş öncesi bilinen bir kimlik giriş sonrasında da geçerli kalır.
- Oturumlar sunucu tarafında kayıt olarak tutulur ve kimliğin karması saklanır; kapatılabilen şey kaydı tutulan şeydir.
- Bir hesabın eşzamanlı oturumları listelenebilir, tek tek kapatılabilir ve sıfırlama sırasında toplu olarak sonlandırılabilir.
- Boşta kalma sınırı ile mutlak ömür ayrı sorunları çözer ve birlikte uygulanır.
- Çerez nitelikleri kimliğin taşınmasını korur; sunucu tarafındaki aygıt denetimi ikinci katmandır ve sıkılığı kullanıcı davranışıyla dengelenir.
Sonraki Adım
Bu konu boyunca soru tekti: sunucu, karşısındakinin kim olduğunu nasıl güvenle biliyor?
Oturum silme sorgusundaki AND uye = ? koşulu ise başka bir soruyu işaret etti — kim
olduğu bilinen birinin neyi yapabileceği. Sonraki konu bu ikinci soruya geçiyor ve rol
tabanlı erişim denetimiyle başlıyor: izinlerin kişiye değil göreve bağlanması, rol
devralmasının etkin izin kümesini nasıl gizlediği ve kütüphane personelinin yetkilerinin
bir çizelgeden nasıl türetildiği.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.