- Cookie HTTP adalah mekanisme dasar untuk mempertahankan state di web, tetapi browser, server, dan standard library berbeda dalam karakter yang diizinkan dan penanganan error, sehingga bisa berujung pada gangguan nyata
- Keluarga RFC 6265 memiliki syarat yang berbeda antara nilai
Set-Cookieyang dikirim server dan nilai yang diterima browser, sementara nilai yang dibuat lewatdocument.cookieberbenturan dengan asumsi parser di sisi server - Firefox, Chromium, dan Safari berbeda satu sama lain dalam penanganan spasi, tanda kutip, koma, backslash, dan Unicode; Safari bahkan menunjukkan perilaku menyimpan hanya bagian awal cookie, bukan seluruh cookie, saat menemui karakter terlarang
- Go bisa diam-diam menghilangkan cookie JSON yang diterima browser, Python
SimpleCookiebisa menghentikan pemuatan setelah cookie yang tidak dipahaminya, dan PHP, Ruby, serta Rust juga memiliki cakupan izin yang berbeda-beda - Satu cookie Unicode dapat menyebabkan error 400/500 atau gangguan parsial di situs besar seperti Facebook, Netflix, Okta, WhatsApp, AWS, dan Apple Support, sehingga spesifikasi cookie dan perilaku library perlu diselaraskan dengan lebih jelas
Cookie yang Diterima Browser tetapi Tidak Bisa Dibaca Go
- Cookie adalah data yang disetel oleh
document.cookiedi JavaScript atau oleh server HTTP, dan terus disertakan dalam request HTTP yang cakupannya sesuai sampai kedaluwarsa - Contoh JavaScript menyimpan string JSON apa adanya sebagai nilai cookie sesi
- Nilainya berbentuk
{"ginger":"snap","peanutButter":"chocolate chip","snicker":"doodle"} - Saat memasukkan JSON ke cookie, sering kali dilakukan serialisasi base64, tetapi browser dapat menyetel nilai ini tanpa masalah dan mengirimkannya sebagai header
Cookie
- Nilainya berbentuk
- Masalah muncul ketika cookie ini diteruskan ke kode yang memakai standard library Go
- Parser Go tidak dapat menafsirkan cookie tersebut
- Kegagalan merambat berantai ke lapisan stack yang lebih tinggi
Dua Standar di Dalam RFC Tidak Selaras
- Cookie didefinisikan melalui RFC 2109, RFC 2965, dan RFC 6265, serta memiliki draft version yang sedang diperbarui
- RFC memperlakukan nilai cookie secara berbeda di dua area
- Section 4.1.1 mengecualikan karakter kontrol, spasi, tanda kutip ganda, koma, titik koma, backslash, dan sebagainya dari nilai yang dikirim server lewat
Set-Cookie - Section 5.6 membuat browser menerima rentang yang jauh lebih luas saat mem-parsing string
Set-Cookie, selama bukan karakter kontrol
- Section 4.1.1 mengecualikan karakter kontrol, spasi, tanda kutip ganda, koma, titik koma, backslash, dan sebagainya dari nilai yang dikirim server lewat
- Konflik utamanya adalah nilai yang harus dikirim server dan nilai yang harus diterima browser tidak selaras
- Jika browser hanya menerima cookie yang disetel oleh server itu sendiri, dampaknya kecil, tetapi
document.cookiejuga bisa membuat cookie - Standar tidak menetapkan dengan jelas apakah standard library yang menangani header
Cookieharus permisif seperti user agent atau ketat seperti server
- Jika browser hanya menerima cookie yang disetel oleh server itu sendiri, dampaknya kecil, tetapi
Perbedaan Nilai Cookie yang Diizinkan Antar-Browser
-
Firefox
- Pemeriksaan nilai cookie Firefox mengizinkan sebagian karakter yang dilarang oleh RFC 6265
- Karakter yang diizinkan meskipun direkomendasikan RFC untuk dikecualikan adalah sebagai berikut
0x09horizontal tab0x20spasi0x22tanda kutip ganda0x2Ckoma0x5Cbackslash
- Perilaku ini dimasukkan dulu untuk menjaga kompatibilitas dengan Chrome dan masih tersisa di kedua codebase
- Pengaturan
network.cookie.blockUnicodedapat menolak nilai0x80ke atas, dan pekerjaan terkait dilacak di bug 1797231 - Masalah izin
0x7Fdiperbaiki di Firefox 108 melalui bug 1797235
-
Chromium
- Chromium hanya menolak karakter kontrol dan titik koma dalam nilai cookie
- Sedikit lebih ketat daripada Firefox karena tidak menerima
0x09horizontal tab - Berbeda dari RFC, Chromium dapat menerima dan mengirim ulang spasi, tanda kutip ganda, koma, backslash, dan karakter Unicode
-
Safari / WebKit
- Kode penyimpanan cookie Safari berada di dalam
CFNetworkyang closed source, sehingga sulit diperiksa langsung - Hasil pengujian dengan menyetel nilai cookie dari
0x00sampai0xFFlewat JavaScript menunjukkan bahwa Safari mengizinkan nilai berikut0x09horizontal tab0x20spasi0x22tanda kutip ganda0x5Cbackslash
- Safari tidak mengizinkan
0x7Fdelete serta karakter0x80-FFhigh ASCII / Unicode - RFC mengatakan seluruh cookie harus diabaikan saat menemui karakter kontrol, tetapi Safari menerima nilai hanya sampai sebelum titik karakter terlarang ditemukan
- Bug Safari yang menghapus spasi di sekitar koma juga teramati saat menyetel nilai
-- , --
- Kode penyimpanan cookie Safari berada di dalam
Perbedaan Parsing pada Bahasa dan Standard Library
-
Go
- Kode cookie Go berperilaku relatif dekat dengan teks RFC untuk nilai yang dikirim server lewat
Set-Cookie - Spasi dan koma yang umum dalam penggunaan nyata diizinkan, tetapi tanda kutip ganda, titik koma, dan backslash tidak diizinkan
- Jika header
Cookiecontoh berisi cookie JSON, hasilrequest.Cookies()di Go hanya menyisakancookie1=foodancookie3=bar cookie2yang diterima browser diam-diam hilang tanpa exception atau error eksplisit
- Kode cookie Go berperilaku relatif dekat dengan teks RFC untuk nilai yang dikirim server lewat
-
PHP
- PHP tidak memiliki fungsi parsing cookie native, sehingga sulit memastikan cakupan izin yang tepat, tetapi hasil pengujian menunjukkan perilaku penanganan karakter kontrol tidak konsisten
- Nilai seperti
0x00-0x09dan0x0Dcarriage return berfungsi - Jika memakai
0x10data link escape atau0x7Fdelete, PHP mengeluarkan error 400 Bad Request - Cookie Unicode juga muncul dalam output pengujian
-
Python
http.cookies.SimpleCookiemilik Python diam-diam menghentikan pemuatan cookie berikutnya saat menemui cookie JSON- Pada input contoh, output hanya menyisakan
cookie1=foo - Jika subdomain dapat menyetel cookie bermasalah pada domain utama, satu cookie tersebut dapat merusak pemrosesan cookie seluruh situs
- Penanganan karakter kontrol juga tidak beraturan
- Sebagian karakter kontrol dimuat sebagai nilai kosong
- Jika
aaditambahkan di depan dan belakang nilai, cookie karakter kontrol tidak dimuat
-
Ruby
CGI::Cookie.parsemilik Ruby tampaknya berperilaku sangat permisif saat parsing- Ia menerima karakter kontrol, tab, tanda kutip ganda, koma, backslash,
0x7F, dan karakter Unicode, lalu menerapkan percent-encoding saat mengambilnya dari cookie jar - Pendekatan ini mungkin mendekati optimal di dunia cookie, tetapi kode yang menyetel nilai lewat
document.cookiemungkin tidak mengharapkan nilai pantulan yang sudah di-percent-encoding
-
Rust
- Rust tidak menyediakan fitur penanganan cookie bawaan, sehingga pemeriksaan dilakukan berdasarkan crate
cookieyang populer - Dengan konfigurasi default, crate
cookiemendekati sisi paling permisif dan tampaknya menerima string UTF-8 yang diberikan
- Rust tidak menyediakan fitur penanganan cookie bawaan, sehingga pemeriksaan dilakukan berdasarkan crate
Dampak yang Terlihat di Situs Web Nyata
- Masalah ini ditemukan saat memverifikasi pembaruan library pihak ketiga secara manual di situs pengujian
- Perubahan ini sulit tertangkap oleh pengujian otomatis
- Jika langsung dirilis, pengunjung berikutnya bisa menerima cookie yang rusak dan terkunci oleh error yang tidak jelas sampai pembaruan di-rollback dan cookie dihapus
- Masalah ini tidak terbatas pada situs kecil atau framework tertentu
- Jika cookie Unicode disetel pada domain melalui konsol browser seperti berikut, berbagai situs besar bisa rusak
document.cookie="unicodeCookie=🍪; domain=.grayduck.mn; Path=/; SameSite=Lax"
- Kasus yang teramati adalah sebagai berikut
- Facebook: halaman error ditampilkan dan gambar juga rusak
- Instagram dan Threads: error 500 sederhana terjadi
- Netflix: mengembalikan error
NSES-500dan halaman bantuan juga rusak - Okta: semua halaman login mengembalikan error 400
- WhatsApp: “whatsapp error” ditampilkan
- Amazon: sebagian besar berfungsi, tetapi beberapa fitur rusak secara acak
- AWS: konsol login mengembalikan error 400 dan berhenti
- Apple Support: gagal memuat daftar perangkat
- Best Buy: navigasi berfungsi, tetapi fungsi pencarian tidak berfungsi
- eBay: sebagian besar sudah diperbaiki, tetapi sebagian masih mengeluarkan error 400
- Home Depot: direncanakan akan diperbaiki
- Intuit: satu-satunya situs yang mengidentifikasi penyebab error
- Outlook: muncul kasus error 400 lainnya
Sulitnya Memperbaiki antara Standar dan Kompatibilitas
- Memperbaiki masalah pada spesifikasi dasar berusia 30 tahun sangat sulit, dan kemungkinan tidak ada solusi yang benar-benar bagus untuk masalah ini
- Opsi memblokir cookie semacam ini di sisi browser telah ditinjau dan dikerjakan oleh Mozilla maupun Google
- Mozilla: bug 1797235, CVE-2023-5723, bug 1797231
- Google: bug 40061459
- Pemblokiran sepihak rumit karena masalah kompatibilitas
- Cookie non-ASCII tidak umum, kurang dari 0,01% dari seluruh cookie
- Ada telemetry yang menunjukkan cookie tersebut jauh lebih sering muncul di negara seperti Argentina, Meksiko, dan Finlandia
- Mozilla mengimplementasikan pengaturan
network.cookie.blockUnicodeyang bisa diaktifkan dengan cepat, tetapi tidak mengaktifkannya karena masalah kompatibilitas perilaku dengan Chromium
- Perbaikan di sisi server juga mungkin, tetapi cakupannya meliputi jutaan situs web serta penanganan error internal bahasa dan framework
- Tempat seperti Facebook atau Netflix mungkin bisa melakukan mitigasi, tetapi operator situs rata-rata sulit memiliki waktu atau kemampuan untuk menyelesaikannya
- Solusi mendasarnya adalah IETF HTTP Working Group menyelaraskan spesifikasi cookie secara internal dan menetapkan secara ketat bagaimana sistem pemrosesan cookie harus bekerja
- Apakah karakter non-ASCII diizinkan harus sama di sisi server dan user agent
- Tahap-tahap browser, bahasa, dan framework dalam memproses cookie juga harus dibuat eksplisit seperti standar W3C modern semacam Content Security Policy
- Perilaku yang membuat satu cookie salah menghentikan pemrosesan cookie lain sulit diterima karena dapat menyebabkan berbagai gangguan tak terduga
Prosedur Penanganan Cookie yang Diusulkan
- Mulai dari
field-value, pisahkan dengan;dan,untuk membuat daftarraw-cookie-pair, tetapi jangan perlakukan koma sebagai sinonim titik koma - Setiap
raw-cookie-pairdiproses dalam urutan berikut- Jika tidak ada
=, lanjut ke pair berikutnya - Hapus spasi di awal dan akhir
- Bagian sebelum
=pertama diperlakukan sebagaicookie-name-octets, sedangkan bagian setelahnya sebagaicookie-value-octets - Jika nilai diawali tanda kutip ganda, hapus satu tanda kutip ganda pembuka, dan jika ada tanda kutip ganda penutup, hapus satu
- Jika nama atau nilai berbentuk tidak dapat diterima server, lewati pair tersebut
- Tuple
[cookie-name-octets, cookie-value-octets]yang tersisa diproses dengan cara yang ditentukan server
- Jika tidak ada
- Server juga diusulkan untuk menolak tuple yang nama cookie-nya bukan token, dan menolak nilai cookie yang berisi octet yang tidak ada dalam
cookie-octet
1 komentar
Komentar Hacker News
Cookie penuh dengan jebakan aneh dan perilaku yang merepotkan, tetapi 99,95% waktunya tetap berjalan baik. Ladang ranjau cookie favorit saya adalah cookie shadowing, yaitu ketika cookie dengan nama yang sama diatur dengan atribut utama seperti domain dan path yang berbeda, maka beberapa cookie yang nyaris sama bisa muncul bersamaan, dan di backend maupun JS tidak ada cara untuk membedakan mana yang mana
Coba buka https://example.com/somepath lalu masukkan ini di konsol browser
document.cookie = "foo=a";document.cookie = "foo=b; domain=.example.com";document.cookie = "foo=c; path=/somepath";document.cookieDalam kasus saya hasilnya adalah
'foo=c; foo=a; foo=b'Ini benar-benar kesalahan besar
/somepath, menerima C, yaitu nilai yang paling spesifik dari ketiganya, tampak cukup masuk akal. Karena semua nilai dikembalikan berurutan, kita bisa mengetahui baik nilai per-path maupun nilai global, jadi rasanya ini kompromi terbaikHanya saja saya tidak suka setter
document.cookieyang terasa seperti sihir, tetapi benda ini sudah berumur hampir 30 tahun jadi mau bagaimana lagiMasalah ini muncul lagi baru-baru ini ketika jshttp/cookie memperketat validasi: https://github.com/jshttp/cookie/pull/167
Setelah PR itu, validasinya kembali sedikit dilonggarkan agar mirip dengan kode browser yang disebut dalam tulisan
Perubahan awalnya dimulai ketika kami menemukan bug di kode kami yang membangun header cookie dengan sekadar menggabungkan string tanpa encoding. Kadang ada spasi di dalam nilainya dan request jadi rusak. Untuk menghindari itu, kami ingin menyarankan para developer memakai
serialize()dari jshttp/cookie, tetapi lalu menyadari bahwa validasi di fungsi itu tidak cukup untuk menangkap bug yang kami temukanSaat kami mengusulkan perbaikan, orang lain menemukan bahwa karena validasinya terlalu longgar, seseorang bisa menyisipkan JS ke dalam field nama cookie lalu membuatnya ditafsirkan sebagai nilai di tempat lain. Jadilah jalur code injection yang cukup unik
Tulisan itu menyebut pendekatan Rust, tetapi tidak seperti bahasa lain, pustaka standar Rust tidak menyertakan fitur pemrosesan cookie. Jadi yang sebenarnya dilihat adalah perilaku crate
cookiepihak ketiga, dan itu juga menyertakan opsi percent-encoding seperti Ruby: https://docs.rs/cookie/0.18.1/cookie/Rasanya di dalam protokol HTTP sebenarnya ada sekitar sepuluh ribu protokol berbeda yang tertanam. Browser dan web server menambahkan bermacam-macam fitur, masing-masing punya spesifikasi dan spesifikasi de facto, dan semuanya dikirim di bawah satu payung HTTP yang nyaris umum untuk segala hal
Klien juga tidak bisa menyatakan versi mana dari sepuluh ribu non-spesifikasi ini yang kompatibel dengannya, dan server pun sama. Alasannya spesifikasi tidak bisa di-upgrade adalah karena klien lain tidak akan memahaminya, dan juga tidak ada kompatibilitas mundur
Akibatnya, yang tersisa adalah kekacauan acak yang tidak bisa disepakati atau diperbaiki siapa pun. Karena tidak ada penghentian terencana, kita harus terus membawa keputusan buruk dari masa lalu
Sekitar 10 tahun lalu saya pernah mengimplementasikan sesi berbasis cookie di sebuah proyek, dan sangat menderita saat debugging kenapa autentikasi bekerja di Safari tetapi tidak di Chrome. Saya tidak ingat persis yang mana, tetapi salah satu browser tidak akan mengatur cookie sama sekali jika formatnya tidak sesuai
Saya juga tidak melakukan hal yang terlalu aneh; kalau tidak salah, masalahnya perbedaan
-dan_Set-CookieSaya pernah tidak bisa memakai
camelCasepada key cookie karena masalah iniMeski dicari, saya juga sulit menemukan isu persisnya
Sejak tak lama setelah cookie diperkenalkan, tampaknya penggunaan yang masuk akal selalu dianggap hanya menyimpan opaque token agar server bisa mengenali bahwa ini klien yang sama di lain waktu, sementara semua yang lain disimpan di sisi server
Saya tidak mengerti kenapa fakta bahwa klien pada prinsipnya bisa menangani nilai yang tidak akan pernah dikirim server justru dianggap masalah. Tinggal jangan kirim nilai seperti itu, dan tidak perlu khawatir dengan teka-teki seperti “apa yang terjadi kalau itu dikirim?”
Meski begitu, ini tetap satu-satunya tempat untuk menyimpan opaque token, jadi tetap harus dipakai untuk autentikasi
Parsing header cookie berantakan. “Standar”-nya tidak mencerminkan perilaku yang benar-benar ada di lapangan, format yang diterima berbeda-beda untuk tiap server backend, library, dan framework, dan browser melakukan hal yang berbeda lagi
Jika frontend dan backend bisa dikendalikan sepenuhnya, ini bukan masalah besar, tetapi begitu harus mengintegrasikan hal-hal yang berbeda, situasinya sangat cepat menjadi konyol
Cookie tampak seperti kekacauan besar dan rumit, dan pada saat yang sama hampir mustahil diubah karena kompatibilitas ke belakang. Dalam situasi seperti ini, rasanya lebih masuk akal membuat mekanisme baru yang benar-benar terpisah
Misalnya, mekanisme seperti NewCookie bisa dispesifikasikan ulang dan didesain kembali agar bekerja konsisten sejak awal. Bisa dibekali langkah keamanan modern, spesifikasi yang lebih ketat, dan dukungan Unicode yang benar
Set-Cookie2yang kini telah ditinggalkan: https://stackoverflow.com/q/9462180/3474615Setidaknya untuk sebagian use case, dan tentu saja tidak terintegrasi langsung dengan header
Karena cookie sudah telanjur ada, kita terikat pada cookie
Saya menghabiskan sebulan penuh memburu masalah iOS Safari yang secara acak memakan cookie dari domain yang dikendalikan pelanggan. Saya belum pernah melihat state sesi hilang seperti ini pada domain seperti Google, Twitter, atau Facebook
Sedikit lebih serius, sebaiknya hindari kata cookie dan beri nama yang benar-benar berbeda. Kata cookie sudah membawa terlalu banyak beban
Penulis mulai dari memasukkan hasil
JSON.stringifyke dalam cookie, dan justru mengejutkan bahwa masalahnya bukan karena seseorang memasukkan titik koma ke dalam JSON yang diserialisasiSebagian besar kerumitan di sekitar cookie tampaknya muncul saat mencoba memasukkan input pengguna sewenang-wenang ke dalam cookie. Itu memang tidak seharusnya dilakukan. Jika hanya memakai string ASCII alfanumerik dengan panjang tetap seperti untuk token autentikasi, maka tidak masalah
Saya setuju bahwa ini benar-benar ladang ranjau
Sebagai developer, cara mengakalinya adalah dengan mengenkode nilainya sebagai Base64 aman-URL. Dengan begitu kita mendapatkan nilai byte mentah dan representasi internalnya bisa dipakai sesuka hati. Tetapi seperti yang disebutkan dalam tulisan, tetap tidak bisa dikendalikan 100%. Karena ini adalah user agent, memang seharusnya begitu
Akan lebih baik jika lebih banyak user agent memilih kepatuhan pada standar daripada “byte di atas kabel dan doa”. Respons 400 pada tangkapan layar itu memang sesuai spesifikasi. Akan lebih baik jika header sejak awal memakai UTF-8, atau awalnya ASCII lalu kemudian diizinkan UTF-8. Namun yang pertama sulit secara hubungan sebab-akibat, dan yang kedua juga tetap bisa menimbulkan masalah karena membuat nilai yang tadinya ilegal menjadi legal
base64urltidak kompatibel denganbase64yang ditambah URL encoding dalam sekitar 3% kasus; saat development ini mudah terlewat, tetapi di production pasti akan meledak=,/,+, jadi encoding Base64 standar juga bisa dipakai :)Tulisan ini mengejek hukum Postel, tetapi jika pihak yang menetapkan cookie bersikap konservatif saat mengirim, sejak awal tulisan seperti ini tidak akan diperlukan
Terkadang ranjau itu bukan sekadar bug sederhana, melainkan celah keamanan besar
Jika klien mengirim data yang tidak sesuai spesifikasi, itu adalah bug dan harus diperbaiki. Server sama sekali tidak boleh menjadikan menebak maksud lalu menerimanya sebagai sesuatu yang normal