3 poin oleh GN⁺ 2024-11-22 | 1 komentar | Bagikan ke WhatsApp
  • 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-Cookie yang dikirim server dan nilai yang diterima browser, sementara nilai yang dibuat lewat document.cookie berbenturan 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 SimpleCookie bisa 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.cookie di 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
  • 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
  • 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.cookie juga bisa membuat cookie
    • Standar tidak menetapkan dengan jelas apakah standard library yang menangani header Cookie harus permisif seperti user agent atau ketat seperti server

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
      • 0x09 horizontal tab
      • 0x20 spasi
      • 0x22 tanda kutip ganda
      • 0x2C koma
      • 0x5C backslash
    • Perilaku ini dimasukkan dulu untuk menjaga kompatibilitas dengan Chrome dan masih tersisa di kedua codebase
    • Pengaturan network.cookie.blockUnicode dapat menolak nilai 0x80 ke atas, dan pekerjaan terkait dilacak di bug 1797231
    • Masalah izin 0x7F diperbaiki 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 0x09 horizontal 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 CFNetwork yang closed source, sehingga sulit diperiksa langsung
    • Hasil pengujian dengan menyetel nilai cookie dari 0x00 sampai 0xFF lewat JavaScript menunjukkan bahwa Safari mengizinkan nilai berikut
      • 0x09 horizontal tab
      • 0x20 spasi
      • 0x22 tanda kutip ganda
      • 0x5C backslash
    • Safari tidak mengizinkan 0x7F delete serta karakter 0x80-FF high 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 -- , --

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 Cookie contoh berisi cookie JSON, hasil request.Cookies() di Go hanya menyisakan cookie1=foo dan cookie3=bar
    • cookie2 yang diterima browser diam-diam hilang tanpa exception atau error eksplisit
  • 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-0x09 dan 0x0D carriage return berfungsi
    • Jika memakai 0x10 data link escape atau 0x7F delete, PHP mengeluarkan error 400 Bad Request
    • Cookie Unicode juga muncul dalam output pengujian
  • Python

    • http.cookies.SimpleCookie milik 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 aa ditambahkan di depan dan belakang nilai, cookie karakter kontrol tidak dimuat
  • Ruby

    • CGI::Cookie.parse milik 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.cookie mungkin tidak mengharapkan nilai pantulan yang sudah di-percent-encoding
  • Rust

    • Rust tidak menyediakan fitur penanganan cookie bawaan, sehingga pemeriksaan dilakukan berdasarkan crate cookie yang populer
    • Dengan konfigurasi default, crate cookie mendekati sisi paling permisif dan tampaknya menerima string UTF-8 yang diberikan

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-500 dan 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
  • 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.blockUnicode yang 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 daftar raw-cookie-pair, tetapi jangan perlakukan koma sebagai sinonim titik koma
  • Setiap raw-cookie-pair diproses dalam urutan berikut
    • Jika tidak ada =, lanjut ke pair berikutnya
    • Hapus spasi di awal dan akhir
    • Bagian sebelum = pertama diperlakukan sebagai cookie-name-octets, sedangkan bagian setelahnya sebagai cookie-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
  • 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

 
GN⁺ 2024-11-22
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.cookie
    Dalam kasus saya hasilnya adalah 'foo=c; foo=a; foo=b'

    • Entah siapa yang merancangnya di perusahaan, tapi lingkungan staging dan development ditempatkan di domain yang sama, dan seluruh perusahaan besar mengikuti pola ini
      Ini benar-benar kesalahan besar
    • Saya jadi berpikir, banyak perilaku aneh saat memakai beberapa akun di satu situs web pada browser yang sama mungkin bisa dijelaskan oleh ini
    • Jika berada di /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 terbaik
      Hanya saja saya tidak suka setter document.cookie yang terasa seperti sihir, tetapi benda ini sudah berumur hampir 30 tahun jadi mau bagaimana lagi
    • Sebagai catatan, secara teknis titik di depan domain tidak diperbolehkan dan akan diabaikan: https://www.rfc-editor.org/rfc/rfc6265#section-4.1.2.3
      Masalah 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 temukan
      Saat 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
    • Betul, ini memang penuh risiko. https://www.usenix.org/conference/usenixsecurity15/technical-sessions/presentation/zheng membahas masalah ini dan kerepotan terkait secara mendalam
  • 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 cookie pihak ketiga, dan itu juga menyertakan opsi percent-encoding seperti Ruby: https://docs.rs/cookie/0.18.1/cookie/

    • Ini contoh bagaimana nama yang bagus direbut lebih awal lalu menjadi standar de facto
  • 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

    • Perangkat middleware buruk yang memblokir protokol yang tidak mereka pahami juga ikut jadi penyebab. Logikanya semacam “lebih aman kalau default-nya memblokir dan gagal”, sehingga mulai sekarang sampai selamanya semua traffic aplikasi baru harus ditunnel di atas HTTP agar bisa berjalan di internet nyata
    • Sejujurnya sekarang saya sudah berdamai dengan dunia seperti ini, dan mungkin malah lebih menyukainya daripada dunia dengan penghentian terencana
    • Jika kita tidak ingin perusahaan monopoli menetapkan spesifikasi yang rapi lalu memaksakan penghentian sesuka hati, maka sebagai gantinya kita harus menanggung anarki
  • 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 _

    • Sepertinya dulu ada perbedaan soal case sensitivity antara Safari dan Chrome. Mungkin terkait header Set-Cookie
      Saya pernah tidak bisa memakai camelCase pada key cookie karena masalah ini
      Meski 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?”

    • Cookie adalah teknologi lama. Ini salah satu hal paling awal yang diperkenalkan pada era 90-an saat web masih muda, dan beberapa ide buruk sudah berulang kali muncul
      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

    • Menarik bahwa NewCookie disebut, karena sebenarnya sudah ada header Set-Cookie2 yang kini telah ditinggalkan: https://stackoverflow.com/q/9462180/3474615
    • NewCookie kira-kira setara dengan Local Storage di browser
      Setidaknya untuk sebagian use case, dan tentu saja tidak terintegrasi langsung dengan header
    • Masalah intinya tampaknya adalah bahwa cookie terlalu dalam terikat dengan pelacakan. Jika sekarang mencoba membuat cookie yang lebih baik, kemungkinan besar itu akan ditolak oleh para pendukung privasi yang tidak ingin konsep seperti itu ada sama sekali
      Karena cookie sudah telanjur ada, kita terikat pada cookie
    • Tempat paling aman untuk menyimpan state di sisi klien adalah DOM dan URL. Ini memang tidak mencakup semua use case, tetapi mencakup hal-hal seperti area saat mengklik tautan pra-persetujuan di email
      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
    • Namanya harus lebih baik daripada NewCookie. Usulan seperti SuperCookie, UltraCookie, atau BetterCookie juga mungkin
      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.stringify ke dalam cookie, dan justru mengejutkan bahwa masalahnya bukan karena seseorang memasukkan titik koma ke dalam JSON yang diserialisasi
    Sebagian 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

    • Saat mengatakan Base64 aman-URL, harus benar-benar dijelaskan secara spesifik maksudnya apa. Encoding base64url tidak kompatibel dengan base64 yang ditambah URL encoding dalam sekitar 3% kasus; saat development ini mudah terlewat, tetapi di production pasti akan meledak
    • Nilai cookie bisa memuat karakter =, /, +, 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

    • Memang pantas diejek. Hukum Postel adalah ide yang mengerikan dan telah menciptakan ladang ranjau di mana-mana
      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
    • Masalah hukum Postel justru adalah bahwa pengirim tidak pernah benar-benar konservatif. Detail perilaku yang diterima oleh sebagian besar penerima pada akhirnya akan dipakai oleh pengirim