Topik CORS ini adalah topik security web yang sulit dimengerti bagi sebagian orang. Dalam pentest, kadang temuan CORS diperdebatkan mengenai severity-nya. Di tulisan ini saya akan berusaha menjelaskan apa itu CORS dan dampaknya untuk security.
Contents
Dasar Teori
Saya ingin sekali bisa menjelaskan CORS ini secara singkat, supaya nggak perlu ngetik panjang, tapi kenyataannya dibutuhkan pemahaman dulu mengenai request di browser. Jadi sebelum masuk masalah securitynya, saya jelaskan dulu mengenai CORS.
Sebelum ada XHR (XMLHTTPRequest)
Sebelum JavaScript mulai dipakai, yang bisa dibuat dengan HTML hanyalah menyisipkan konten dengan menggunakan tag yang ada, misalnya memakai <img src="">
atau <frame src="">
. Konten ini (misalnya gambar) boleh berasal dari domain mana saja. Browser akan meminta konten dari domain lain. Hidup masih sangat sederhana.
Ketika JavaScript mulai dipakai, kemampuannya masih terbatas untuk memodifikasi DOM (Document Object Model). Ini pun masih belum jadi masalah. Javascript hanya sekedar membuat elemen HTML dan browser yang akan melakukan request ke server tujuan. JavaScript tidak memproses data secara langsung.
Lalu Microsoft membuat yang namanya XMLHTTPRequest, sekarang kode JavaScript bisa meminta data yang akand diproses tanpa harus ditampilkan di browser. Untuk data yang asalnya dari server yang sama, ini cukup sederhana, tapi biasanya jadi rumit ketika kita memakai domain/subdomain yang berbeda.
HTTP Header
Ketika melakukan request ke server, ada beberapa hal yang perlu dikirimkan:
- VERB atau dikenal juga REQUEST METHOD, misalnya GET/POST/OPTIONS
- Path (misalnya /admin, bisa juga mengandung parameter, misalnya /view?id=1)
- Header (misalnya ‘User-Agent’, ‘Cookie’)
- Data (untuk request yang mengirimkan data)
Di dalam bagian header inilah kita akan memasukkan Cookie dan juga header untuk CORS.
Cookie
Untuk sesuatu yang sifatnya publik, maka kita tidak butuh cookie. Misalnya kita pergi ke kebun binatang, bagian loket adalah tempat umum untuk bertanya, tidak perlu bayar kalau tidak masuk ke dalam.
Cookie adalah data yang dikirim server ke client/browser. Lalu browser akan menyimpan data ini, dan ketika kita melakukan request berikutnya ke server yang sama maka Cookie akan dikirimkan oleh browser ke server tersebut.
Secara awam, analogi cara kerja cookie itu seperti ini:
- Kita daftar membership ke Gym dan mendapatkan kartu dengan nomor ID tertentu (ini seperti cookie). Waktu mendaftar mungkin kita ditanya informasi diri (Ini seperti kita memasukkan username/password di halaman login)
- Ketika masuk Gym, kita cuma perlu menunjukkan kartu anggota, dan akan diterima selama dianggap masih aktif oleh gym
Jadi untuk orang yang tidak punya kartu anggota, ketika akan masuk Gym akan segera ditolak. Di dalam Browser, yang terjadi juga sama: jika tidak punya Cookie akan langsung ditolak.
Cookie memiliki masa aktif, dan server bisa memutuskan bahwa suatu cookie dianggap tidak aktif lagi karena suatu hal. Dalam hal ini: di browser masih ada Cookienya dan dikirim ke server, tapi server menolak Cookienya.
Mungkin kita punya banyak kartu untuk kunjungan ke dokter, salon, dan berbagai tempat lain. Tentunya kita akan cukup pintar: kalau ke gym ya menyerahkan kartu untuk Gym, bukan kartu yang dipakai dokter. Tapi mungkin Gym ini ada banyak cabang, jadi kartunya bisa dipakai juga untuk Gym lain.
Seperti halnya kartu membership yang kadang bisa dipakai di lebih dari satu tempat, cookie juga bisa dikirimkan browser ke situs lain. Ada aturannya yang lebih detail (misalnya apakah cookie bisa ke domain yang sama, ke protokol yang sama, ke path yang sama). Bagian ini tidak akan saya bahas dengan detail.
Perlu diperhatikan bahwa dengan sistem seperti ini: kalau ada yang bisa dapat kartu kita, maka bisa disalin dan kartunya dipakai orang lain. Ini sama juga dengan Cookie, jika kita bisa mendapatkan Cookie seseorang, maka (besar) kemungkinan kita bisa login menjadi orang tersebut.
Tentunya kita akan pusing jika kita punya puluhan kartu dan harus mengingat tiap tempat butuh kartu yang mana. Jadi bayangkan ada sebuah alat yang bisa otomatis mengurusi semua kartu kita, dan bisa pintar memberikan kartu yang benar ke tempat yang benar, dan juga akan membuang kartu otomatis jika sudah tidak berlaku lagi.
Sebenarnya inilah yang dilakukan browser. Browser akan menyimpan cookie dan dengan pintar memberikan Cookie yang tepat ke website yang tepat, dan menghapus cookie yang sudah expire.
Kadang di suatu tempat (misalnya Kebun Binatang atau Dunia Fantasi), kita diberi tiket, dan itu hanya berlaku di hari itu saja, sampai kita keluar. Ini sebenarnya sama dengan Cookie yang hanya berlaku sampai timeout atau window ditutup.
Credentials
Cookie hanyalah salah satu dari credential yang mungkin dipakai oleh browser. Selain Cookie, ada 2 lagi yang bisa dipakai:
- Header Authorization
- SSL Certificate
Untuk menyederhanakan, saya akan menggunakan Cookie saja untuk mewakili semua credential yang mungkin.
Resource Sharing
Ini semua masih cukup jelas dan sederhana jika setiap domain memakai cookie untuk domainnya sendiri. Tapi ini mulai jadi rumit ketika skrip di sebuah domain membutuhkan akses dari origin berbeda.
Mari kita teruskan analogi tadi, misalnya Gym ini dibolehkan menghubungi dokter. Ini bisa sekedar memesan brosur dari dokter untuk ditampilkan di lobi. Dalam kasus ini siapa saja memang boleh meminta brosur dari dokter, dan brosur ini tidak bisa diedit teksnya oleh Gym. Ini seperti website meminta gambar dari site lain.
Sekarang bagaimana jika permintaannya lebih rumit: kita ingin minta file Excel dari dokter yang bisa kita proses. Misalnya daftar tips selama bulan puasa untuk ditampilkan di TV yang ada di dalam Gym. Dalam kasus ini: meskipun ini data publik, tapi belum tentu diijinkan oleh dokternya.
Tapi mungkin kita ingin mengijinkan permintaan yang lebih spesifik tentang diri kita. Bayangkan pihak Gym bisa menghubungi dokter untuk meminta data kesehatan kita untuk menyarankan olahraga terbaik. Caranya: Gym akan meminta koneksi ke dokter, via alat ajaib yang kita miliki, dan alat ajaib ini akan menghubungi dokter dengan kartu kita untuk dokter tersebut.
Mari kita bayangkan dulu seperti apa analogi di browser. Kita sudah login di sebuah situs. Misalnya site1. Jadi browser punya cookie untuk site1, dan jika ada request ke site1, maka Cookie akan otomatis dikirim.
Nah bagaimana jika kita mengunjungi site2, tapi site ini ada JavaScriptnya, dan script tersebut mengirimkan request ke site2? Kalau hanya sekedar meminta gambar dari site2, browser akan segera mengirimkan requestnya. Tapi jika yang meminta dari JavaScript, maka ini akan melalui proses lain.
Tentunya berbahaya sekali kalau semua tempat bisa menghubungi dokter, menggunakan kartu kita, karena bisa meminta data pribadi kita. Bahkan jika semua boleh meminta data, maka situs akan jadi berat. Jadi ada mekanisme keamanan untuk mencegah sembarang tempat bisa menghubungi tempat lain dengan atau tanpa memakai kartu pribadi kita.
Mekanisme keamanannya bentuknya seperti ini: sebelum Gym diijinkan menghubungi dokter dengan memakai kartu kita, maka alat ajaib ini akan menghubungi dulu dokter, nanya: ini Gym mau akses data pribadi user dengan kartunya user, boleh nggak? kalau dibolehkan, maka Gym ini bisa memakai alat kita untuk menghubungi dokter, dan alat akan otomatis mengirimkan kartu identifikasi ke dokter.
Di browser Request ini di browser disebut sebagai preflight request. Jadi sebelum request yang sesungguhnya dikirimkan, browser mengirim dulu request ke server, dan jika dibolehkan, maka baru akan mengirimkan requestnya.
Mekanisme keamanan yang memungkinkan berbagai pihak berbagi (sharing) data (resource) antara Origin yang berbeda (cross origin) — dalam contoh ini Gym dan Dokter — yang dilakukan oleh browser ini yang disebut dengan CORS.
Origin
Origin meliputi skema/protokol (http/https/protokol lain), nama domain, dan port. Deskripsi lengkap mengenai origin bisa dibaca di RFC. Secara sederhana: jika semuanya sama persis, maka dianggap dari origin yang sama. Jika ada yang berbeda maka dianggap origin berbeda (jadi walau protokol/domain sama, tapi port berbeda, maka dianggap origin berbeda).
Request Sederhana
Request sederhana (istilah teknisnya: simple request) adalah request yang responsenya tidak akan diproses oleh JavaScript, seperti jaman awal web dulu (segala macam GET/POST yang datanya tidak akan dibaca/proses oleh JS). Ada banyak syarat sebuah request dianggap sederhana. Semua syarat ini harus dipenuhi:
- Methodnya harus GET, HEAD atau POST
- Header tambahan yang dibolehkan (selain default dari browser):
Accept
,Accept-Language
Content-Language
danContent-Type
- Untuk
Content-Type
yang diperbolehkan hanyaapplication/x-www-form-urlencoded
,multipart/form-data
dantext/plain
- Jika memakai
XMLHttpRequest
, maka tidak boleh ada listenernya untuk upload - API
ReadableStream
tidak boleh dipakai di request
Request sederhana ini tidak butuh header CORS. Jadi tidak akan ada preflight request.
Preflight Request
Untuk request selain simple request, ketika ditanya: apakah Origin x, boleh mengakses resource di Origin lain, maka browser akan mengirimkan request dengan verb/request method “OPTIONS” dengan menyatakan apa “Origin” yang meminta seperti ini:
OPTIONS /path/resource
Origin: https://site1
Access-Control-Request-Method: POST
Access-Control-Request-Headers: header1, header2
Jadi sebelum browser mengirimkan request, maka browser bertanya dulu ke server: ini script di origin http://site1
mau mengirim request POST
dengan header header1
dan header2
ke path /path/resource
. Boleh nggak?
Dan server akan menjawab dengan berbagai header, yang intinya adalah mengijinkan atau tidak. Header yang dikirim adalah:
Access-Control-Allow-Origin
yang menyatakan origin yang dibolehkanAccess-Control-Allow-Methods
yang menyatakan method yang dibolehkanAccess-Control-Allow-Headers
yang menyatakan header apa yang diijinkanAccess-Control-Allow-Credentials
yang menyatakan apakah credential boleh dikirimkan
Contoh reply adalah seperti ini:
HTTP/1.1 200 OK Content-Length: 0 Connection: keep-alive Access-Control-Allow-Origin: https://site1 Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE Access-Control-Allow-Headers: Content-Type, header1, header2
Untuk kode javascript yang melakukan simple request, ini agak berbeda:
- Request akan tetap dilakukan (tanpa preflight request)
- Jika
Access-Control-Allow-Origin
mengijinkan, maka Javascript bisa mendapatkan balasannya
Masalah Security Access-Control-Allow-Origin
Yang menjadi masalah paling umum adalah mengenai Access-Control-Allow-Origin
. Jika kita mulai membuat aplikasi yang mengakses resource di origin berbeda, maka aplikasinya tidak jalan jika server tidak membalas sama sekali preflight request.
Jadi biasanya seseorang akan membuat jawaban untuk preflight request ini, dan biasanya untuk memudahkan development maka dibuat: Pokoknya semuanya boleh. Karena malas mengurusi domain development
, uat
, production
.
Access-Control-Allow-Origin: *
Salah satu yang akan dicoba pertama oleh programmer adalah menggunakan *
untuk Access-Control-Allow-Origin
. Hasilnya: semua situs bisa akses ini, tapi kemudian akan disadari bahwa ini tidak kompatibel dengan Access-Control-Allow-Credentials: true
(tidak diperbolehkan).
Jadi untuk data yang memang mau dibiarkan terbuka di internet, bisa diakses oleh JavaScript di situs manapun, maka menggunakan *
tidak apa-apa. Tapi ini bermasalah untuk situs internal. Bahayanya adalah: situs external bisa meminta data ke situs internal/intranet, walau terbatas tanpa Cookie/Credential. Misalnya ini mungkin bisa digunakan untuk membaca berita internal perusahaan.
Reflected Access-Control-Allow-Origin
Berikutnya: karena belum tahu domain mana saja yang akan diijinkan di produksi, biasanya yang dilakukan oleh programmer adalah memakai origin di pertanyaan untuk mengisi jawaban. Jadi kalau ditanya: “Apakah origin X boleh mengakses?” “Ya, domain X boleh mengakses”. Tidak peduli X-nya itu apa.
Nah setting ini berbahaya jika digabung dengan Access-Control-Allow-Credentials: true
, yang artinya: Cookie/Credentials boleh dikirimkan. Artinya situs manapun bisa mengirim request ke situs tersebut, memakai Cookie/Credential user. Jika tanpa Access-Control-Allow-Credentials: true
maka ini efeknya sama dengan origin *
.
Jika request dari domain insecure http://example.com
dibolehkan oleh situs https://example.com
, maka ini juga berbahaya. Artinya jika ada orang bisa melakukan MITM terhadap domain non secure (hanya port 80, port 443 dipassthough ke domain asli) maka orang ini bisa mengakses domain yang secure.
Access-Control-Allow-Origin: null
Header ini agak berbahaya. Browser mungkin akan membolehkan dokumen HTML di lokal (yang tidak punya domain) mengakses situs yang mengirimkan Access-Control-Allow-Origin: null
. Jadi sebaiknya server jangan mengembalikan null
.
Error yang berhubungan dengan CORS
Programmer banyak yang bingung menangani error CORS. Ini melibatkan dua pihak: front end yang meminta request, dan backend yang memproses. Apa yang saya tulsikan di sini hanya versi singkat error yang mungkin. Untuk developer, sebaiknya bacalah petunjuk ini (CORS for Developers).
Error pertama adalah di bagian preflight request: Server mungkin tidak membalas, atau memberikan balasan yang salah. Dalam kasus ini, maka browser tidak akan mengirimkan request ke server.
Server juga bisa salah implementasinya, misalnya jika kita memakai middleware yang menambahkan header otomatis. Padahal jika ada lebih dari 1 header Allow, maka akibatnya jadi error. Preflight request juga tidak boleh diredirect ke URL lain, jika diredirect maka tidak akan diproses oleh browser.
Penutup
Semoga tulisan ini cukup memperjelas apa itu CORS, dan apa risiko dalam kesalahan header CORS. Topik CORS ini memang gampang-gampang susah, sampai ada satu buku khusus membahas CORS saja. Untuk kasus security lain yang berhubungan dengan CORS, artikel dari PortSwigger ini cukup bagus.