Keamanan Siber

CORS untuk Aplikasi Web Kecil - Tentukan Origin Browser yang Boleh Membaca Response

CORS untuk Aplikasi Web Kecil - Tentukan Origin Browser yang Boleh Membaca Response

Console browser mengatakan sebuah request “blocked by CORS.” API yang sama mungkin tetap mengembalikan 200 OK di terminal, dan menambahkan Access-Control-Allow-Origin: * seolah membuat error itu menghilang. Sebenarnya, apa yang diizinkan header tersebut?

Jawaban yang berguna lebih sempit daripada “akses lintas domain.” CORS adalah protokol browser untuk memutuskan apakah code dari satu origin boleh mengakses response dari origin lain. CORS bukan firewall umum untuk endpoint, dan tidak menentukan apakah seorang pengguna sudah terautentikasi atau boleh mengedit record tertentu. Menjaga batas-batas ini tetap terlihat membuat debugging dan security review tidak terlalu membingungkan.

Mulai dari origin, bukan hanya domain

Untuk URL HTTP dan HTTPS biasa, sebuah origin dikenali dari kombinasi scheme, host, dan port. RFC 6454 menjelaskan model origin ini sebagai batas perlindungan yang digunakan oleh user agent. Path tidak membuat origin terpisah, tetapi perubahan scheme, host, atau port dapat melakukannya.

  • https://app.example/account dan https://app.example/settings merupakan origin yang sama.
  • http://app.example dan https://app.example merupakan origin yang berbeda karena scheme-nya berbeda.
  • https://app.example dan https://api.example merupakan origin yang berbeda karena host-nya berbeda.
  • https://app.example dan https://app.example:8443 merupakan origin yang berbeda karena port-nya berbeda.

Itulah sebabnya “tetapi kedua layanan dimiliki perusahaan yang sama” tidak menyelesaikan error CORS. Kepemilikan organisasi bukan bagian dari perbandingan origin oleh browser. Browser melihat dua domain perlindungan; server harus menjelaskan dengan sengaja setiap bentuk berbagi di antara keduanya.

CORS mengendalikan akses browser terhadap response

Same-origin policy secara umum mencegah script membaca informasi milik origin lain. Namun, web tidak pernah melarang semua jenis pengiriman lintas origin. Link melakukan navigasi antar-situs, gambar dimuat dari situs lain, dan form HTML dapat dikirim ke origin lain. RFC 6454 membedakan batas yang lebih ketat untuk membaca dari sejarah pengiriman yang lebih longgar.

CORS adalah pelonggaran terkendali atas batas pembacaan tersebut. Misalkan JavaScript yang dimuat dari https://app.example mengambil data publik dari https://api.example/status. Browser menambahkan request header Origin:

GET /status HTTP/1.1
Host: api.example
Origin: https://app.example

Jika API ingin code browser dari origin tersebut dapat mengakses response, API dapat menjawab:

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://app.example
Vary: Origin

{"status":"available"}

Browser membandingkan kebijakan pada response dengan origin yang meminta. Jika pemeriksaan CORS berhasil, script pemanggil dapat memakai response. Jika header yang diperlukan tidak ada atau tidak cocok, browser menahan response tersebut dari script.

Kalimat terakhir itu memiliki batas penting: pemeriksaan CORS yang gagal tidak membuktikan bahwa server tidak pernah menerima atau memproses request. Beberapa request lintas origin dapat dikirim tanpa pemeriksaan awal. Karena itu, desain aplikasi yang memahami CORS tetap harus melindungi tindakan di sisi server meskipun tidak ada response yang dibagikan kepada JavaScript.

Preflight menanyakan request yang akan dikirim

Beberapa request lintas origin memakai request OPTIONS yang dibuat browser sebelum request sebenarnya. Inilah CORS preflight. Preflight umumnya muncul ketika code ingin memakai method seperti PUT atau DELETE, request header yang tidak termasuk safelist, atau content type di luar CORS safelist. Kondisi tepatnya berada dalam aturan safelist pada Fetch Standard yang bersifat living standard, sehingga ringkasan pendek tidak boleh dianggap sebagai spesifikasi lengkap.

Untuk update JSON yang membawa header Authorization, sebuah preflight dapat terlihat seperti ini:

OPTIONS /profile HTTP/1.1
Host: api.example
Origin: https://app.example
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type

Response dengan cakupan sempit dapat berupa:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: Authorization, Content-Type
Vary: Origin

Jika response memenuhi pemeriksaan CORS browser, browser dapat mengirim PUT yang sebenarnya. Pertukaran ini merupakan negosiasi protokol, bukan bukti bahwa pengguna boleh memperbarui profile. Handler yang sebenarnya tetap harus mengautentikasi pemanggil, mengotorisasi akses ke profile yang dipilih, memvalidasi body, dan menerapkan kontrol integritas request yang dibutuhkan threat model-nya.

Preflight juga tidak dijamin mendahului setiap request lintas origin. Sebuah POST yang masuk safelist, termasuk request yang bentuknya seperti pengiriman form HTML tradisional, dapat dikirim tanpanya. Merancang endpoint destruktif dengan asumsi “browser pasti melakukan preflight” menempatkan keputusan keamanan endpoint pada lapisan yang salah.

Credentials membuat keputusan berbagi lebih sensitif

fetch() lintas origin tidak menyertakan credentials secara default. Code client harus memilihnya, misalnya dengan credentials: "include", dan server harus secara eksplisit mengizinkan pembagian response ber-credentials. Menurut aturan credentials dalam Fetch Standard, server tidak dapat menggabungkan response ber-credentials dengan Access-Control-Allow-Origin: *. Server memerlukan origin tertentu yang diizinkan dan field response berikut:

Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Credentials: true
Vary: Origin

Hal ini tidak memaksa browser mengirim cookie. Domain, path, Secure, SameSite, dan kebijakan third-party cookie browser tetap menjadi batas terpisah. Panduan MDN tentang credentialed request secara eksplisit mencatat bahwa kebijakan third-party cookie biasa tetap berlaku. Header CORS tidak dapat menimpanya.

Pembagian ber-credentials layak memakai allowlist yang lebih kecil karena response yang dapat dibaca mungkin memuat informasi khusus pengguna. Jika sebuah endpoint benar-benar publik dan tidak memakai credentials, * dapat secara akurat menyatakan akses publik yang dapat dibaca browser. Wildcard bukan bug dengan sendirinya; wildcard tidak tepat ketika audiens yang dimaksud lebih sempit daripada semua origin.

Jangan memberi CORS pekerjaan yang tidak dapat dilakukannya

Beberapa pertanyaan keamanan berada di dekat CORS, tetapi tetap berdiri sendiri:

  • Authentication: Siapa yang membuat request? Session, token, client certificate, atau mekanisme lain menjawab pertanyaan ini.
  • Authorization: Bolehkah identitas tersebut menjalankan operasi ini pada object ini? Aplikasi harus memutuskannya untuk setiap tindakan yang dilindungi.
  • CSRF protection: Apakah browser mengirim request yang mengubah state tanpa dikehendaki menggunakan ambient credentials? Token, pengaturan cookie yang tepat, dan pemeriksaan konteks request menangani ancaman ini.
  • CORS: Bolehkah code browser dari origin ini mengakses response lintas origin ini?

PortSwigger memperingatkan bahwa CORS bukan perlindungan CSRF. Form lintas origin dapat mengirim request tertentu tanpa mendapatkan izin untuk membaca response. Sebaliknya, origin frontend yang diizinkan dengan benar tetap tidak boleh memperoleh record yang tidak berhak dilihat pengguna yang sedang login.

CORS juga tidak membatasi HTTP client langsung seperti CORS membatasi JavaScript di browser. Command-line client, backend service, atau script otomatis dapat memanggil endpoint yang dapat dijangkau lewat internet tanpa menerapkan pemeriksaan response CORS milik browser. Authentication, authorization, rate limit, dan network control harus tetap berdiri sendiri.

Bangun allowlist sebagai kebijakan, bukan hiasan string

Ketika lebih dari satu frontend sah, server dapat membandingkan Origin yang masuk dengan allowlist exact dan hanya mengembalikan nilai itu ketika cocok. Menyalin begitu saja setiap Origin yang diberikan ke Access-Control-Allow-Origin mengubah validasi semu menjadi izin bagi setiap origin.

Pemeriksaan suffix yang longgar juga mudah disalahpahami. Pemeriksaan yang hanya diakhiri dengan example.com dapat menerima host yang tidak dimaksudkan seperti notexample.com. Scheme dan port juga penting. Origin serialized yang exact lebih mudah diaudit:

https://app.example
https://admin.example
https://preview.example:8443

Nilai serialized null dapat mewakili opaque origin atau privacy-sensitive origin dalam beberapa konteks browser. Nilai ini sebaiknya tidak ditambahkan sembarangan sebagai jalan pintas untuk file lokal atau sandboxed document. Jika sebuah produk benar-benar membutuhkannya, keputusan kepercayaan itu layak mendapat threat analysis sendiri, bukan disatukan ke dalam “izinkan development.”

Ketika server memilih origin yang diizinkan secara dinamis, Vary: Origin memberi tahu cache bahwa pemilihan response bergantung pada Origin dalam request. Tanpa pembedaan tersebut, shared cache dapat memakai kembali metadata response yang dipilih untuk satu origin ketika melayani origin lain. Field ini mendukung ketepatan cache; ia tidak menggantikan allowlist.

Uji kebijakan dari kedua sisi

Terminal berguna untuk memeriksa cara server menjawab origin dan preflight yang diajukan:

curl -i https://api.example/status \
  -H 'Origin: https://app.example'

curl -i -X OPTIONS https://api.example/profile \
  -H 'Origin: https://app.example' \
  -H 'Access-Control-Request-Method: PUT' \
  -H 'Access-Control-Request-Headers: authorization, content-type'

Command ini tidak membuktikan bahwa browser akan mengekspos response akhirnya. curl menampilkan apa yang dikirim server; ia tidak menerapkan same-origin policy milik browser. Selesaikan pengujian di browser sungguhan dari setiap origin frontend yang dituju.

Matriks pengujian kecil sebaiknya mencakup:

  1. Origin yang diizinkan beserta method dan header yang diharapkan.
  2. Origin yang tidak terdaftar, yang tidak boleh menerima nilai allow-origin pemberi akses.
  3. Host yang tampak mirip serta scheme atau port yang salah.
  4. Origin yang tidak ada, mengikuti perilaku normal non-CORS endpoint tersebut.
  5. Request ber-credentials dan tanpa credentials sebagai kasus terpisah.
  6. Method yang ditolak dan custom header yang ditolak dalam preflight.
  7. Response sukses, validation error, authentication failure, dan server error, karena header yang tidak konsisten dapat membuat hanya sebagian response tidak terbaca.
  8. Response melalui CDN atau reverse proxy, dengan pemeriksaan bahwa Vary: Origin dan nilai allow-origin yang dipilih tetap diteruskan dengan benar.

Console browser dapat menjelaskan mengapa pemeriksaan CORS gagal, sedangkan panel Network dapat menunjukkan apakah preflight terjadi dan response header mana yang datang. Log server menjawab pertanyaan berbeda: apa yang benar-benar diterima dan diproses endpoint. Membaca ketiga sudut pandang ini mencegah kita mendiagnosis browser policy error sebagai API outage, atau melewatkan tindakan di server hanya karena JavaScript tidak dapat membaca response-nya.

Buat batasnya eksplisit

Kebijakan CORS yang cermat dimulai dari inventaris: origin browser mana yang membutuhkan response mana, apakah credentials terlibat, serta method dan request header mana yang benar-benar diperlukan. Resource publik dapat menyatakan akses publik. Integrasi privat sebaiknya memakai origin exact dan permukaan sekecil yang praktis. Endpoint yang tidak memerlukan akses browser lintas origin tidak membutuhkan header CORS yang permisif.

Pertanyaan utamanya bukan “Bagaimana cara menghilangkan error CORS?” Pertanyaannya adalah “Code browser mana yang boleh membaca response ini, dan kontrol independen apa yang melindungi operasinya?” Setelah keduanya dijawab secara terpisah, CORS menjadi protokol berbagi yang presisi, bukan sakelar keamanan yang misterius.

References