Ders 01 / 15
Çerezler
İstekler arasında durumu istemcide taşıyan çerezin Set-Cookie başlığı ve imzalı biçimi; bir sonucu kimin değiştirebildiğini sınıflayan dört taraf ve on sonuçlu ölçüm.
Yayın Güncelleme
Bu derste
- Çerezin durumu istemcide taşıdığını ve her istekle geri geldiğini açıklar
- Bir sonucu sunucu, ortam, istemci ve üçüncü taraf olarak sınıflayan kuralı ve karar sırasını okur
- On sonuçlu ölçümü koşturup 4, 2, 2 ve 2 dağılımını yorumlar
- Set-Cookie başlığını niteliklerle kurar ve imzalı çerezin değiştirilen değeri fark ettiğini, ama sonucun sahibini değiştirmediğini ölçer
İçindekiler
Nesneye Dayalı PHP ve Standartlar kursu kodu birden çok dosyaya ve yazara böldü ve bir tanım değişince kullananın ne yaşadığını ölçtü. Bu kurs kodu web’e çıkarır: istek bir tarayıcıdan gelir, ayar dağıtıldığı ortamdan, fiyat bir dış servisten. Kodu yazan artık sonucun tek sahibi değildir. Bu kurs her düzeneği tek soruyla ölçer: bu sonucu kim değiştirebiliyor?
İlk durak, web’in durumu nerede taşıdığıdır. HTTP durumsuzdur: sunucu her isteği öncekinden bağımsız işler (HTTP Ailesi dersleri). Bir sepetin ya da bir kullanıcının istekler arasında hatırlanması için durum bir yerde durmak zorundadır. En eski yer çerezdir (cookie): sunucu yanıtta bir Set-Cookie başlığı gönderir, istemci değeri saklar ve aynı siteye giden her istekte geri yollar. Durum istemcide durur ve her istekle sunucuya döner.
Ölçüm Çekirdeği
Ölçümün kendisi taraf.php dosyasında durur ve kurs boyunca değişmez. Her derste aynı dosya yeniden verilir.
<?php // taraf.php — bu sonucu kim değiştirebiliyor? declare(strict_types=1); // Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım. const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam']; /** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */ function sina(callable $sonuc, array $sapmalar): array { $taban = $sonuc([]); $degisti = []; foreach (DIS_TARAFLAR as $taraf) { if (array_key_exists($taraf, $sapmalar)) { $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban; } } return $degisti; } /** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */ function taraf(array $degisti): string { foreach (DIS_TARAFLAR as $t) { if ($degisti[$t] ?? false) return $t; } return 'sunucu'; } function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string { $degisti = sina($sonuc, $sapmalar); $satirlar = []; foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi'); $sonTaraf = taraf($degisti); echo "$ad → $sonTaraf\n ", implode(' · ', $satirlar), "\n"; return $sonTaraf; }
DIS_TARAFLAR kodun dışındaki üç tarafı karar sırasıyla sayar: istemci, üçüncü taraf, ortam. sina() bir sonucu hesaplayan fonksiyonu alır, onu önce sapmasız çağırır ($taban), sonra her dış tarafın sapmasıyla ayrı ayrı çağırır ve sonucun tabandan farklı olup olmadığını kaydeder. taraf() bu kayıtlara sırayla bakar ve değiştirebilen ilk dış tarafı verir. tarafRaporu() sonucu adlandırır, sapmaları ve sonucu basar.
DO1. Karar sırası taraf()’ın kendisidir: önce istemci, sonra üçüncü taraf, sonra ortam; hiçbiri sonucu değiştiremediyse sunucu. Sıra saldırı yüzeyinin genişliğidir. İstemci isteği gönderen herkestir, üçüncü taraf yalnız uygulamanın bağlandığı taraflardır, ortam yalnız dağıtımı yapanın elindedir. Sunucu sapma listesinde yoktur, çünkü kod her sonucu değiştirebilir; sorulan, kodun dışında kimin değiştirebildiğidir. Taraf bir etiket değil bir kuraldır: geliştirici sonucu adlandırır, her ilgili dış taraftan tek bir sapma tanımlar ve taraf bu iki girdiden çıkar.
| Taraf | Tanım |
|---|---|
| sunucu | Sonucu yalnız uygulamanın kodu belirler; hiçbir dış taraf tek başına değiştiremez |
| ortam | Aynı kod, dağıtıldığı ortam değişince (ortam değişkeni, bağlantı ayarı, çalışma zamanı sürümü, veritabanının durumu) farklı sonuç verir |
| istemci | İsteği gönderen taraf (çerez, form alanı, sorgu parametresi, sunduğu belirteç) sonucu değiştirir |
| üçüncü taraf | Uygulamanın çağırdığı bir servis ya da kullanıcının tarayıcısına istek gönderten başka bir site sonucu değiştirir |
İki ayrım baştan yapılmalıdır. Nesneye Dayalı PHP ve Standartlar kursunda bir tanım değişikliği öncesi ve sonrasıyla ölçüldü; burada tanım sabittir, değişen dış bir tarafın girdisidir. C ile Sistem Programlama kursunda bir değişikliği gören taraf, gözlemci arandı; burada yön terstir ve değişikliği yapan taraf aranır.
On Sonuç, Dört Taraf
sonuclar.php kursun on sonucunu bir kez ölçer. Bu dosya yalnız bu derste yazılır; sonraki dersler taraf.php’yi çeker ve yalnız kendi sonucunu kurar.
<?php // sonuclar.php — on sonuç, dört taraf declare(strict_types=1); require 'taraf.php'; putenv('UYG_VERITABANI=uygulama.db'); function ortamda(?string $deger, callable $f): mixed { if ($deger === null) return $f(); $eski = getenv('UYG_VERITABANI'); putenv("UYG_VERITABANI=$deger"); try { return $f(); } finally { putenv("UYG_VERITABANI=$eski"); } } $db = new PDO('sqlite::memory:'); $db->exec('CREATE TABLE uyeler (ad TEXT, yas INTEGER)'); $db->exec("INSERT INTO uyeler VALUES ('ayse', 30), ('mehmet', 41)"); function hazirDeyim(PDO $db, string $ad): string { $deyim = $db->prepare('SELECT yas FROM uyeler WHERE ad = ?'); $deyim->execute([$ad]); return $deyim->queryString; } const BELIRTEC_UZUNLUGU = 16; $belirtec = bin2hex(random_bytes(BELIRTEC_UZUNLUGU)); function epostaIsle(array $istekler, ?string $belirtec): string { $eposta = '[email protected]'; foreach ($istekler as $istek) { if ($belirtec !== null && !hash_equals($belirtec, $istek['belirtec'] ?? '')) continue; $eposta = $istek['eposta']; } return $eposta; } $kullaniciIstegi = ['koken' => 'https://ornek.test', 'belirtec' => $belirtec, 'eposta' => '[email protected]']; $baskaSiteden = ['koken' => 'https://baska.test', 'eposta' => '[email protected]']; $cerez = ['sepet' => '2']; $oturum = ['sepet' => 2]; $servisYaniti = '{"fiyat": 120}'; $calismaVarsayilani = ['cost' => 12]; // PHP 8.4 ve sonrası bcrypt varsayılanı $sonuclar = [ ['sepet (çerezde)', fn(array $s) => (int) ($s['istemci'] ?? $cerez)['sepet'], ['istemci' => ['sepet' => '5']]], ['sepet (oturumda)', fn(array $s) => $oturum['sepet'], ['istemci' => ['sepet' => '5']]], ['sorgu metni (birleştirme)', fn(array $s) => "SELECT yas FROM uyeler WHERE ad = '" . ($s['istemci'] ?? 'ayse') . "'", ['istemci' => 'mehmet']], ['sorgu metni (hazır deyim)', fn(array $s) => hazirDeyim($db, $s['istemci'] ?? 'ayse'), ['istemci' => 'mehmet']], ['e-posta (belirteçsiz)', fn(array $s) => epostaIsle([$kullaniciIstegi, ...($s['üçüncü taraf'] ?? [])], null), ['üçüncü taraf' => [$baskaSiteden]]], ['e-posta (belirteçli)', fn(array $s) => epostaIsle([$kullaniciIstegi, ...($s['üçüncü taraf'] ?? [])], $belirtec), ['üçüncü taraf' => [$baskaSiteden]]], ['fiyat (dış servis)', fn(array $s) => json_decode($s['üçüncü taraf'] ?? $servisYaniti, true, 4, JSON_THROW_ON_ERROR)['fiyat'], ['üçüncü taraf' => '{"fiyat": 125}']], ['karma öneki (varsayılan)', fn(array $s) => substr(password_hash('parola', PASSWORD_BCRYPT, $s['ortam'] ?? $calismaVarsayilani), 0, 7), ['ortam' => ['cost' => 10]]], ['karma öneki (açık maliyet)', fn(array $s) => substr(password_hash('parola', PASSWORD_BCRYPT, ['cost' => 12]), 0, 7), ['ortam' => ['cost' => 10]]], ['veritabanı adı', fn(array $s) => ortamda($s['ortam'] ?? null, fn() => getenv('UYG_VERITABANI')), ['ortam' => 'deneme.db']], ]; $sayac = ['sunucu' => 0, 'ortam' => 0, 'istemci' => 0, 'üçüncü taraf' => 0]; foreach ($sonuclar as [$ad, $sonuc, $sapmalar]) { $sayac[tarafRaporu($ad, $sonuc, $sapmalar)]++; } foreach ($sayac as $t => $n) echo "$t $n\n";
sepet (çerezde) → istemci
istemci değiştirdi
sepet (oturumda) → sunucu
istemci değiştiremedi
sorgu metni (birleştirme) → istemci
istemci değiştirdi
sorgu metni (hazır deyim) → sunucu
istemci değiştiremedi
e-posta (belirteçsiz) → üçüncü taraf
üçüncü taraf değiştirdi
e-posta (belirteçli) → sunucu
üçüncü taraf değiştiremedi
fiyat (dış servis) → üçüncü taraf
üçüncü taraf değiştirdi
karma öneki (varsayılan) → ortam
ortam değiştirdi
karma öneki (açık maliyet) → sunucu
ortam değiştiremedi
veritabanı adı → ortam
ortam değiştirdi
sunucu 4
ortam 2
istemci 2
üçüncü taraf 2
DO2. On sonucun 4’ü sunucu, 2’si ortam, 2’si istemci, 2’si üçüncü taraf çıktı. Bu dağılım kursun bağlayıcı sayısıdır; sonraki derslerin hiçbiri onu değiştirmez, her ders kendi kesitini ölçer. Çıktının okunuşu çiftlerden başlar. On sonucun sekizi dört çift oluşturur ve her çiftte bir düzenek sonucu bir dış taraftan sunucuya taşır:
- Sepet çerezde durduğunda istemcinin gönderdiği
5sepeti değiştirdi; oturumda durduğunda aynı sapma değiştiremedi. - Sorgu metni birleştirmeyle kurulduğunda nötr bir ad (
mehmet) metni değiştirdi; hazır deyimde deyim metni aynı kaldı. - Belirteçsiz e-posta güncellemesinde başka bir siteden gelen istek e-postayı değiştirdi; belirteçli güncellemede
hash_equals()o isteği geçirmedi. - Karma öneki ortamın sağladığı varsayılan maliyetle üretildiğinde ortam öneki değiştirdi; maliyet açıkça verildiğinde değiştiremedi.
İki sonucun sahibi hiçbir düzenekle değişmez: dış servisin fiyatı her zaman üçüncü tarafındır ve veritabanı adı her zaman ortamındır. Bu ikisinde savunma sahibi değiştirmez, sonucu yalnız sınırlar.
Set-Cookie Başlığı
Çerezin sahibi kimse onu bir başlıkla kurar. Komut satırında başlık listesi boştur, bu yüzden aşağıda başlık gönderilmez, bir dizge olarak kurulur ve basılır. İmzalı çerez için iki küçük fonksiyon ayrı bir dosyada durur.
<?php // cerez.php — imzalı çerez değerini üreten ve doğrulayan iki fonksiyon declare(strict_types=1); const IMZA_ANAHTARI = 'ders-anahtari'; function imzala(string $deger): string { return $deger . '.' . hash_hmac('sha256', $deger, IMZA_ANAHTARI); } function cerezDegeri(string $cerez, string $varsayilan): string { [$deger, $imza] = array_pad(explode('.', $cerez, 2), 2, ''); $beklenen = hash_hmac('sha256', $deger, IMZA_ANAHTARI); return hash_equals($beklenen, $imza) ? $deger : $varsayilan; }
imzala() değeri hash_hmac('sha256', …) ile anahtarlı bir karmaya (imza) bağlar ve değer.imza biçiminde birleştirir. cerezDegeri() istemciden gelen çerezi böler, imzayı yeniden hesaplar ve hash_equals() ile karşılaştırır; imza tutmazsa varsayılan değeri verir. IMZA_ANAHTARI bir ders sabitidir.
<?php // baslik.php — Set-Cookie başlığı ve imzalı sepet çerezinin ölçümü declare(strict_types=1); require 'taraf.php'; require 'cerez.php'; $gecerli = imzala('2'); $baslik = 'Set-Cookie: sepet=' . $gecerli . '; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Lax'; echo str_replace(substr($gecerli, 2), '<imza>', $baslik), "\n"; $degistirilmis = '5' . substr($gecerli, 1); echo 'değiştirilmiş değer imzadan geçti mi: ', cerezDegeri($degistirilmis, '0') === '5' ? 'evet' : 'hayır', "\n"; tarafRaporu( 'sepet (imzalı çerez, değiştirilmiş değer)', fn(array $s) => cerezDegeri($s['istemci'] ?? $gecerli, '0'), ['istemci' => $degistirilmis], );
Set-Cookie: sepet=2.<imza>; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Lax
değiştirilmiş değer imzadan geçti mi: hayır
sepet (imzalı çerez, değiştirilmiş değer) → istemci
istemci değiştirdi
DO3. Başlıktaki nitelikler tarayıcıya istektir. Path=/ çerezin hangi yollarda geri döneceğini, Max-Age=3600 ne kadar saklanacağını söyler. HttpOnly sayfadaki betiğin çerezi okumasını, Secure çerezin yalnız şifreli bağlantıda gönderilmesini, SameSite=Lax çerezin başka bir sitenin isteğiyle birlikte gönderilmesini sınırlamayı ister. Üçü de sunucunun yazdığı bir dizgedir; uygulanması istemcidedir ve sunucu bunu bu derste ölçemez.
Ölçüm iki şey gösterir. Değiştirilmiş değer imzadan geçmedi ve cerezDegeri() varsayılana, yani 0’a düştü. İmza çalıştı: sunucu değiştirilen değeri kabul etmedi. Ama sonuç yine değişti: 2 iken 0 oldu ve değişikliği yapan istemciydi. Taraf istemcidir.
Sınırlayıcı Ölçüm
İmzanın sınırı şudur: imza bir değerin anahtarla üretildiğini gösterir, değerin güncel olduğunu göstermez. İstemci elinde daha önce aldığı geçerli bir çerezi, kendi önceki sepetini geri gönderirse imza geçer mi?
<?php // eski.php — istemci önceki geçerli çerezini geri gönderirse declare(strict_types=1); require 'taraf.php'; require 'cerez.php'; $guncel = imzala('2'); $eski = imzala('1'); echo 'eski çerez imzadan geçti mi: ', cerezDegeri($eski, '0') === '1' ? 'evet' : 'hayır', "\n"; tarafRaporu( 'sepet (imzalı çerez, eski geçerli değer)', fn(array $s) => cerezDegeri($s['istemci'] ?? $guncel, '0'), ['istemci' => $eski], );
eski çerez imzadan geçti mi: evet
sepet (imzalı çerez, eski geçerli değer) → istemci
istemci değiştirdi
DO4. Geçti. İmza değerin anahtarla üretildiğini kanıtlar, değerin güncel olduğunu kanıtlamaz: eski çerez geçerli bir imza taşıdığı için sunucu onu kabul etti ve sepet 2 yerine 1 oldu. Sonuç yine istemcidir. İmza istemcinin üretebileceği değerleri sunucunun önceden imzaladıklarına daraltır; hangisini göndereceğini seçmek istemcide kalır.
Savunma her zaman sahibi değiştirmez. Bu derste imzalı çerez, istemcinin ürettiği bir değeri geçerli kümeye daraltır; sonucu istemcinin elinden almaz. Sahibi değiştirmek için durumun istemcide durmaması gerekir.
Özet
- Çerez, HTTP’nin durumsuzluğunu durumu istemcide tutarak aşar; sunucu
Set-Cookieile yazar, istemci her istekte geri gönderir. - Kurs her düzeneği tek soruyla ölçer: bu sonucu kim değiştirebiliyor. Karar sırası istemci, üçüncü taraf, ortam; hiçbiri değiştiremezse sunucu.
- Çekirdek on sonucun 4’ünü sunucu, 2’sini ortam, 2’sini istemci, 2’sini üçüncü taraf olarak sınıfladı; bu, çekirdeğin dağılımıdır, derslerin dağılımı değil.
Set-Cookienitelikleri tarayıcıya istektir; imzalı çerez değiştirilen değeri fark eder ama sonucu sunucuya taşımaz.- Bu dersin ölçü sınıfı istemci’dir: imzalı çerezde de sonucun sahibi, değeri gönderen istemcidir.
Sonraki Adım
Çerezde sepet istemcide durdu ve istemci onu değiştirebildi. Sonraki ders, Oturumlar, değeri sunucuya taşıyor: sepet $_SESSION’da duruyorsa aynı sapma sonucu değiştirebiliyor mu ve çerezde yalnızca bir kimlik kaldığında o kimliği kim belirliyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.