Çerez işlemek mayın tarlasıdır
(grayduck.mn)- HTTP çerezleri web’de durumu korumanın temel mekanizmasıdır, ancak tarayıcılar, sunucular ve standart kütüphaneler izin verilen karakterler ve hata işleme konusunda farklı davrandığından gerçek kesintilere yol açabilir
- RFC 6265 ailesinde sunucunun gönderdiği
Set-Cookiedeğerleri ile tarayıcının kabul ettiği değerlerin koşulları farklıdır;document.cookieile oluşturulan değerler sunucu tarafı ayrıştırıcıların varsayımlarıyla çakışır - Firefox, Chromium ve Safari boşluk, tırnak, virgül, ters eğik çizgi ve Unicode işleme konusunda birbirinden farklıdır; Safari yasaklı bir karakterle karşılaştığında çerezin tamamını değil, yalnızca baştaki kısmını kaydetme davranışı gösterir
- Go, tarayıcının kabul ettiği JSON çerezlerini sessizce atlayabilir; Python
SimpleCookieanlayamadığı bir çerezden sonraki yüklemeyi durdurabilir; PHP, Ruby ve Rust’ta da izin verilen aralıklar birbirinden farklıdır - Tek bir Unicode çerezi Facebook, Netflix, Okta, WhatsApp, AWS, Apple Support gibi büyük sitelerde 400/500 hatalarına veya kısmi arızalara neden olabilir; bu yüzden çerez spesifikasyonu ile kütüphane davranışlarının daha net uyumlu hale getirilmesi gerekir
Tarayıcının kabul ettiği ama Go’nun okuyamadığı çerez
- Çerezler, JavaScript’te
document.cookieveya bir HTTP sunucusu tarafından ayarlanan verilerdir; süresi dolana kadar kapsamı uyan HTTP isteklerine eklenmeye devam ederler - Örnek JavaScript, JSON dizesini olduğu gibi oturum çerezi değeri olarak kaydeder
- Değer
{"ginger":"snap","peanutButter":"chocolate chip","snicker":"doodle"}biçimindedir - JSON’u çereze koyarken çoğu zaman base64 serileştirme yapılır, ancak tarayıcı bu değeri sorunsuzca ayarlar ve
Cookiebaşlığıyla gönderir
- Değer
- Bu çerez Go standart kütüphanesini kullanan koda iletildiğinde sorun ortaya çıkar
- Go ayrıştırıcısı bu çerezi yorumlayamaz
- Hata, çağrı yığınının üst katmanlarına zincirleme yayılır
RFC içindeki iki ölçüt uyuşmuyor
- Çerezler RFC 2109, RFC 2965, RFC 6265 üzerinden tanımlanmıştır ve şu anda güncellenmekte olan bir taslak sürüm vardır
- RFC, çerez değerlerini iki alanda farklı ele alır
- Section 4.1.1, sunucunun
Set-Cookieile gönderdiği değerlerde kontrol karakterlerini, boşluğu, çift tırnağı, virgülü, noktalı virgülü, ters eğik çizgiyi vb. hariç tutar - Section 5.6, tarayıcının
Set-Cookiedizesini ayrıştırırken kontrol karakterleri dışında çok daha geniş bir aralığı kabul etmesine izin verir
- Section 4.1.1, sunucunun
- Temel çatışma, sunucunun göndermesi gereken değerler ile tarayıcının kabul etmesi gereken değerlerin hizalı olmamasıdır
- Tarayıcı yalnızca sunucunun bizzat ayarladığı çerezleri kabul etseydi etkisi küçük olurdu, ancak
document.cookiede çerez oluşturabilir - Standart,
Cookiebaşlığını işleyen standart kütüphanelerin kullanıcı ajanı gibi hoşgörülü mü, yoksa sunucu gibi katı mı olması gerektiğini net biçimde belirlemez
- Tarayıcı yalnızca sunucunun bizzat ayarladığı çerezleri kabul etseydi etkisi küçük olurdu, ancak
Tarayıcılara göre çerez değeri kabul farkları
-
Firefox
- Firefox’un çerez değeri denetimi, RFC 6265’in yasakladığı bazı karakterlere izin verir
- İzin verilen, RFC’nin dışlanmasını önerdiği karakterler şunlardır
0x09yatay sekme0x20boşluk0x22çift tırnak0x2Cvirgül0x5Cters eğik çizgi
- Bu davranış geçmişte Chrome ile uyumluluğu sağlamak için eklenmiş ve iki kod tabanında da kalmıştır
network.cookie.blockUnicodeayarı0x80ve üzerindeki değerleri reddedebilir; ilgili çalışma bug 1797231 altında izlenir0x7F’e izin verilmesi sorunu bug 1797235 ile Firefox 108’de düzeltilmiştir
-
Chromium
- Chromium çerez değerlerinde yalnızca kontrol karakterlerini ve noktalı virgülü reddeder
- Firefox’tan biraz daha katıdır;
0x09yatay sekmeyi kabul etmez - RFC’den farklı olarak boşluk, çift tırnak, virgül, ters eğik çizgi ve Unicode karakterleri kabul edip yeniden gönderebilir
-
Safari / WebKit
- Safari’nin çerez saklama kodu kapalı kaynaklı
CFNetworkiçinde olduğu için doğrudan doğrulamak zordur - JavaScript ile
0x00’dan0xFF’e kadar çerez değerleri ayarlanarak yapılan kontrolde Safari’nin şu değerleri kabul ettiği görüldü0x09yatay sekme0x20boşluk0x22çift tırnak0x5Cters eğik çizgi
- Safari
0x7Fdelete ile0x80-FFhigh ASCII / Unicode karakterlerine izin vermez - RFC, kontrol karakteriyle karşılaşınca çerezin tamamının yok sayılmasını söyler; Safari ise yasaklı karaktere kadar olan değeri kabul eder
-- , --değeri ayarlandığında virgül çevresindeki boşluğu kaldıran bir Safari hatası da gözlemlenmiştir
- Safari’nin çerez saklama kodu kapalı kaynaklı
Diller ve standart kütüphaneler arasındaki ayrıştırma farkları
-
Go
- Go’nun çerez kodu, sunucunun
Set-Cookieile gönderdiği değerlere ilişkin RFC ifadesine nispeten yakın davranır - Gerçek kullanımda yaygın olan boşluk ve virgüle izin verir, ancak çift tırnak, noktalı virgül ve ters eğik çizgiye izin vermez
- Örnek
Cookiebaşlığında JSON çerezi yer alırsa Go’nunrequest.Cookies()sonucunda yalnızcacookie1=foovecookie3=barkalır - Tarayıcının kabul ettiği
cookie2, istisna veya açık hata olmadan sessizce çıkarılır
- Go’nun çerez kodu, sunucunun
-
PHP
- PHP’de yerel bir çerez ayrıştırma fonksiyonu olmadığı için izin verilen aralığı kesin olarak söylemek zordur, ancak test sonuçlarına göre kontrol karakteri işleme davranışı tutarlı değildir
0x00-0x09ve0x0Dcarriage return gibi değerler çalışır0x10data link escape veya0x7Fdelete kullanıldığında PHP 400 Bad Request hatası verir- Unicode çerezler de test çıktısında görünür
-
Python
- Python’un
http.cookies.SimpleCookiesınıfı bir JSON çereziyle karşılaştığında sonraki çerezlerin yüklenmesini sessizce durdurur - Örnek girdide çıktıda yalnızca
cookie1=fookalır - Bir alt alan adı temel alan adına sorunlu bir çerez ayarlayabiliyorsa, tek bir çerez tüm sitenin çerez işlemesini bozabilir
- Kontrol karakterlerinin işlenmesi de düzensizdir
- Bazı kontrol karakterleri boş değer olarak yüklenir
- Değerin başına ve sonuna
aaeklenirse kontrol karakteri içeren çerez yüklenmez
- Python’un
-
Ruby
- Ruby’nin
CGI::Cookie.parsefonksiyonu ayrıştırma sırasında oldukça hoşgörülü davranıyor gibi görünür - Kontrol karakterlerini, sekmeyi, çift tırnağı, virgülü, ters eğik çizgiyi,
0x7F’i ve Unicode karakterleri kabul eder; çerez jar’ından çıkarırken percent-encoding uygular - Bu yaklaşım çerez dünyasında optimale yakın olabilir, ancak
document.cookieile ayarlayan kod percent-encoding uygulanmış yansıyan değer beklemiyor olabilir
- Ruby’nin
-
Rust
- Rust varsayılan çerez işleme özelliği sunmadığı için popüler
cookiecrate’i temel alınarak kontrol edilmiştir - Varsayılan ayarlarla
cookiecrate’i en hoşgörülü tarafa yakındır ve iletilen UTF-8 dizesini kabul ediyor gibi görünür
- Rust varsayılan çerez işleme özelliği sunmadığı için popüler
Gerçek web sitelerinde ortaya çıkan etki
- Bu sorun, test sitesinde üçüncü taraf kütüphane güncellemesi elle doğrulanırken keşfedildi
- Otomatik testlerde yakalanması zor bir değişiklikti
- Olduğu gibi dağıtılsaydı sonraki ziyaretçiler bozuk bir çerez alabilir ve güncelleme geri alınana ve çerez silinene kadar nedeni belirsiz hatalarla kilitlenebilirdi
- Bu sorun küçük sitelerle veya belirli bir framework ile sınırlı değildir
- Tarayıcı konsolundan aşağıdaki gibi alan adına Unicode çerezi ayarlandığında birçok büyük site bozulabilir
document.cookie="unicodeCookie=🍪; domain=.grayduck.mn; Path=/; SameSite=Lax"
- Gözlemlenen örnekler şöyledir
- Facebook: Hata sayfası gösterilir ve görseller de bozulur
- Instagram ve Threads: Basit bir 500 hatası oluşur
- Netflix:
NSES-500hatası döndürür ve yardım sayfası da bozulur - Okta: Tüm giriş sayfaları 400 hatası döndürür
- WhatsApp: “whatsapp error” gösterilir
- Amazon: Çoğu şey çalışır, ancak bazı özellikler rastgele bozulur
- AWS: Giriş konsolu 400 hatası döndürür ve kesilir
- Apple Support: Cihaz listesini yükleyemez
- Best Buy: Navigasyon çalışır, ancak arama işlevi çalışmaz
- eBay: Çoğu düzeltilmiştir, ancak bazı kısımlar hâlâ 400 hatası verir
- Home Depot: Düzeltilmesi planlanıyor
- Intuit: Hatanın nedenini tespit eden tek site
- Outlook: Başka bir 400 hatası örneği görülür
Standartlar ve uyumluluk arasındaki düzeltme zorluğu
- 30 yıllık temel bir spesifikasyondaki sorunu düzeltmek çok zordur ve bu sorun için iyi bir çözüm bulunmama ihtimali yüksektir
- Tarayıcı tarafında bu tür çerezleri engelleme seçeneği hem Mozilla hem de Google tarafından incelenmiş ve üzerinde çalışılmıştır
- Mozilla: bug 1797235, CVE-2023-5723, bug 1797231
- Google: bug 40061459
- Tek taraflı engelleme uyumluluk sorunları nedeniyle karmaşıktır
- ASCII olmayan çerezler, tüm çerezlerin %0,01’inden azı düzeyinde yaygın değildir
- Telemetry verileri Argentina, Mexico, Finland gibi ülkelerde çok daha sık görüldüğünü gösterir
- Mozilla hızlıca açılabilecek
network.cookie.blockUnicodeayarını uygulamıştır, ancak Chromium ile davranış uyumluluğu sorunları nedeniyle etkinleştirmemiştir
- Sunucu tarafında düzeltme de mümkün olabilir, ancak bu milyonlarca web sitesine ve diller ile framework’lerin iç hata işleme davranışlarına yayılır
- Facebook veya Netflix gibi yerler bunu hafifletebilir, ancak ortalama bir site işletmecisinin çözmek için zamanı veya yetkinliği olması zordur
- Temel çözüm, IETF HTTP Working Group’un çerez spesifikasyonunu kendi içinde hizalaması ve çerez işleme sistemlerinin nasıl davranması gerektiğini katı biçimde belirlemesidir
- ASCII dışı karakterlere izin verilip verilmeyeceği sunucu tarafında ve kullanıcı ajanlarında aynı olmalıdır
- Tarayıcıların, dillerin ve framework’lerin çerezleri işleme adımları da Content Security Policy gibi modern W3C standartlarında olduğu gibi açıkça belirtilmelidir
- Hatalı tek bir çerez yüzünden diğer çerezlerin işlenmesinin de durması, çeşitli beklenmedik arızalara yol açabileceğinden kabul edilmesi zordur
Önerilen çerez işleme prosedürü
field-valueile başlayıp;ve,üzerinden bölerekraw-cookie-pairlistesi oluşturulur; ancak virgül noktalı virgülün eş anlamlısı olarak ele alınmaz- Her
raw-cookie-pairşu sırayla işlenir=yoksa sonraki pair’e geçilir- Baştaki ve sondaki boşluklar kaldırılır
- İlk
=işaretinden öncesicookie-name-octets, sonrasıcookie-value-octetsolarak ele alınır - Değer çift tırnakla başlıyorsa baştaki bir çift tırnak kaldırılır; sonda çift tırnak varsa bir tane kaldırılır
- Ad veya değer sunucunun kabul edemeyeceği biçimdeyse ilgili pair atlanır
- Kalan
[cookie-name-octets, cookie-value-octets]tuple’ı sunucunun tanımladığı yöntemle işlenir
- Sunucular için ayrıca çerez adı token olmayan tuple’ların reddedilmesi ve çerez değeri
cookie-octetiçinde olmayan octet içeriyorsa reddedilmesi yönünde öneri yapılır
1 yorum
Hacker News görüşleri
Çerezler garip tuzaklar ve rahatsız edici davranışlarla dolu, ama %99,95 oranında düzgün çalışıyor. En sevdiğim çerez mayın tarlası cookie shadowing; aynı adla, sadece domain ve path gibi temel öznitelikleri farklı olacak şekilde çerez ayarlarsanız, neredeyse aynı birden fazla çerez aynı anda oluşuyor ve backend ya da JS tarafında hangisinin hangisi olduğunu ayırt etmenin bir yolu yok.
https://example.com/somepath adresine gidip tarayıcı konsoluna aşağıdakileri girmeniz yeterli.
document.cookie = "foo=a";document.cookie = "foo=b; domain=.example.com";document.cookie = "foo=c; path=/somepath";document.cookieBende sonuç
'foo=c; foo=a; foo=b'oldu.Gerçekten inanılmaz büyük bir hata.
/somepathaltındaysanız üç değer içinden en spesifik olan C’yi almak oldukça mantıklı görünüyor. Tüm değerler sırayla döndüğü için, hem path’e özel değeri hem de global değeri görebiliyorsunuz; bu da en iyi uzlaşma gibi hissettiriyor.Yine de o sihirli
document.cookiesetter’ını hiç sevmiyorum ama artık neredeyse 30 yıllık bir şey olduğu için yapacak bir şey yok.jshttp/cookie tarafında doğrulama yakın zamanda sıkılaştırılınca bu konu yeniden gündeme geldi: https://github.com/jshttp/cookie/pull/167
O PR’den sonra doğrulama, yazıda bahsedilen tarayıcı koduna benzer şekilde yeniden biraz gevşetildi.
İlk değişiklik, bizim kodda encoding yapmadan string’leri birleştirerek cookie header oluşturan bir bug bulmamızla başladı. Bazen değerde boşluk oluyordu ve istek bozuluyordu; bunu önlemek için geliştiricilere jshttp/cookie’nin
serialize()fonksiyonunu kullanmalarını önermeyi düşünüyorduk ama bu fonksiyonun doğrulamasının gördüğümüz bug’ı yakalamak için yeterli olmadığını fark ettik.Bir yama önerince başka biri, doğrulamanın fazla gevşek olması nedeniyle çerezin name alanına JS enjekte edilebildiğini ve başka bir yerde bunun value olarak yorumlanabildiğini fark etti. Oldukça sıra dışı bir kod enjeksiyonu yolu ortaya çıkıyordu.
Yazı Rust yaklaşımından söz ediyor ama diğer dillerin aksine Rust standart kütüphanesinde çerez işleme yer almıyor. Aslında bakılan şey üçüncü taraf
cookiecrate’inin davranışı ve Ruby’de olduğu gibi percent-encoding seçeneği de içeriyor: https://docs.rs/cookie/0.18.1/cookie/HTTP protokolünün içinde fiilen birbirinden farklı on bin kadar protokol gömülü gibi duruyor. Tarayıcılar ve web sunucuları türlü türlü özellik ekledi; her birinin hem spesifikasyonu hem de fiili spesifikasyonu var ve bunların hepsi neredeyse tek bir genel amaçlı HTTP şemsiyesi altında taşınıyor.
İstemcinin bu on bin gayriresmî protokolün hangi sürümüyle uyumlu olduğunu belirtmesinin bir yolu yok, sunucunun da yok. Spesifikasyonları yükseltemiyorsunuz çünkü geri kalan istemciler bunu anlamıyor ve geriye dönük uyumluluk da yok.
Sonuçta kimsenin üzerinde anlaşamadığı ve düzeltemediği rastgele bir kaos kalmış durumda. Planlı kullanım dışı bırakma da olmadığı için geçmişteki kötü kararları taşımaya devam etmek zorundayız.
Yaklaşık 10 yıl önce bir projede çerez tabanlı oturum uygulamıştım ve kimlik doğrulamanın Safari’de çalışıp Chrome’da çalışmamasının nedenini debug etmek gerçekten çok uğraştırmıştı. Tam olarak hangisi olduğunu hatırlamıyorum ama tarayıcılardan biri biçim yanlışsa çerezi hiç ayarlamıyordu.
Özellikle tuhaf bir şey de yapmamıştık; aklımda kaldığı kadarıyla mesele
-ile_farkıydı.Set-Cookieheader’ındaydı.Bir zamanlar bu yüzden çerez anahtarlarında
camelCasekullanamadığımız olmuştu.Arayınca da tam sorunu pek bulamıyorum.
Çerezler ortaya çıktığından kısa süre sonra, makul kullanımın sadece opaque token koymak ve sunucunun bir dahaki sefere bunun aynı istemci olduğunu anlamasını sağlamak, geri kalan her şeyi de sunucu tarafında tutmak olduğu düşünülüyordu sanırım.
İstemcinin prensip olarak sunucunun asla göndermeyeceği değerleri işleyebilmesinin neden sorun olduğunu anlamıyorum. Böyle değerleri göndermeyin yeter; “bunu gönderirse ne olur?” gibi bilmecelerle uğraşmaya gerek yok.
Yine de opaque token saklayabileceğiniz tek yer olduğu için kimlik doğrulamada kullanmak zorundasınız.
Çerez başlığını ayrıştırma tam bir karmaşa. “Standart”, sahada gerçekten var olan davranışları yansıtmıyor; backend sunucusu, kütüphane ve framework’lerin kabul ettiği biçimler birbirinden farklı, tarayıcılar da bambaşka şeyler yapıyor
Frontend ve backend’i tamamen kontrol ediyorsanız büyük bir sorun olmayabilir, ama farklı şeyleri birbirine bağlamanız gereken anda durum çok hızlı biçimde saçmalaşıyor
Çerezler büyük ve karmaşık bir keşmekeş gibi görünüyor, üstelik geriye dönük uyumluluk yüzünden neredeyse değiştirmek de imkânsız. Böyle durumlarda tamamen ayrı yeni bir mekanizma yapmak daha doğru olmaz mı diye düşündürüyor
Örneğin NewCookie gibi bir mekanizma yeni baştan tanımlanıp en baştan tutarlı çalışacak şekilde yeniden tasarlanabilir. Modern güvenlik önlemleri yerleşik gelir, daha katı bir spesifikasyon ve düzgün Unicode desteği de eklenebilir
Set-Cookie2başlığı var: https://stackoverflow.com/q/9462180/3474615En azından bazı kullanım senaryolarında öyle; tabii başlıklarla doğrudan entegre değil
Çerezler zaten var olduğu için biz de çerezlere bağlı kalıyoruz
Müşterilerin kontrol ettiği alan adlarındaki çerezleri iOS Safari’nin rastgele yutması sorununu kovalamakla tam bir ay harcadım. Google, Twitter, Facebook gibi alan adlarında oturum durumunun böyle kaybolduğunu hiç görmedim
Biraz daha ciddi söylersek, cookie kelimesinden kaçınıp tamamen başka bir ad vermek daha iyi olur. cookie sözcüğü fazla yük taşıyor
Yazarın işe
JSON.stringifysonucunu çereze koyarak başlamış olması, sorunun birinin string’e çevrilen JSON içine noktalı virgül koymasından kaynaklanmaması açısından daha da şaşırtıcıÇerezlerle ilgili dertlerin çoğu, rastgele kullanıcı girdisini çereze koymaya çalıştığınızda çıkıyor gibi görünüyor. Bunu yapmamalısınız. Kimlik doğrulama token’larında olduğu gibi yalnızca sabit uzunlukta alfasayısal ASCII dizeleri kullanırsanız sorun olmaz
Bunun epey büyük bir mayın tarlası olduğuna katılıyorum
Geliştirici olarak etrafından dolanma yöntemi, değeri URL güvenli Base64 ile kodlamak. Böylece ham bayt değerlerini elde eder, iç temsili de istediğiniz gibi kullanabilirsiniz. Ama yazıda da dendiği gibi, bu %100 kontrol edebildiğiniz bir şey değil. Sonuçta bunlar kullanıcı aracısı ve öyle olmaları da gerekiyor
Keşke daha fazla kullanıcı aracısı, “hat üzerindeki baytlar ve dua” yaklaşımı yerine standartlara uyumu seçseydi. Ekran görüntüsündeki 400 yanıtları spesifikasyona uygun yanıtlar. Başlıklar en baştan UTF-8 olsaydı ya da önce ASCII olup sonradan UTF-8’e izin verilseydi daha iyi olabilirdi. Gerçi ilki nedensellik açısından zor, ikincisi de başlangıçta geçersiz olan değerleri geçerli hâle getirdiği için yine sorun çıkarabilir
base64urlkodlaması,base64üstüne URL kodlaması eklemekle yaklaşık %3 durumda uyumsuzdur; geliştirme sırasında kolayca gözden kaçar ama prod’da mutlaka patlar=,/,+karakterlerini koyabildiğiniz için standart Base64 kodlaması da kullanılabilir :)Yazı Postel yasası ile alay ediyor ama çerezi ayarlayan taraf gönderirken tutucu davransaydı, baştan böyle bir yazıya gerek kalmazdı
Bazen bu mayınlar basit bir bug değil, büyük bir güvenlik açığı da olabiliyor
İstemci spesifikasyona uymayan veri gönderiyorsa bu bir bug’dır ve düzeltilmelidir. Sunucunun niyeti tahmin edip kabul etmesi asla normalleşmemeli