CORS (Cross-Origin Resource Sharing) adalah cara server memberi tahu browser: "halaman dari origin ini boleh membaca jawabanku." Tanpa izin itu, JavaScript di halaman dari origin lain tetap bisa mengirim request, tapi browser gak menyerahkan jawabannya. Bayangin resepsionis kantor. Surat dari tamu mana pun tetap diantar ke dalam dan dibalas, tapi resepsionis cuma menyerahkan balasannya ke tamu kalau di amplopnya tertulis "boleh diberikan ke tamu dari kantor X". Resepsionisnya browser, tulisan di amplopnya header Access-Control-Allow-Origin.
🧪 Tebakan cepat: halaman di
https://evil.commenjalankanfetch('https://api.myapp.com/orders'). API-mu gak memasang header CORS sama sekali. Request itu sampai ke server atau gak? Jawabannya ada di bagian "Yang ditahan browser: jawabannya, bukan request-nya".
Origin: scheme, host, port
Origin adalah gabungan tiga bagian URL: scheme (https), host (myapp.com), dan port (443, port bawaan https). Dua URL punya origin yang sama kalau ketiganya sama persis. Path seperti /orders gak ikut dihitung.
| URL | Sama dengan https://myapp.com? |
Kenapa |
|---|---|---|
https://myapp.com/orders |
sama | cuma path-nya yang beda |
http://myapp.com |
beda | scheme http, bukan https |
https://api.myapp.com |
beda | host beda, subdomain juga dihitung beda |
https://myapp.com:8080 |
beda | port 8080, bukan 443 |
Browser menjalankan same-origin policy (aturan origin yang sama): JavaScript di sebuah halaman cuma boleh membaca jawaban dari origin-nya sendiri. Halaman https://myapp.com yang memanggil https://api.myapp.com sudah termasuk lintas origin (cross-origin), jadi API-nya harus mengizinkan lewat CORS.
Yang ditahan browser: jawabannya, bukan request-nya
Untuk request sederhana seperti GET, browser langsung mengirimnya dan menempelkan header Origin berisi origin halaman:
GET /orders HTTP/1.1
Host: api.myapp.com
Origin: https://evil.com
Server memproses request itu dan menjawab 200 OK beserta datanya. Setelah jawabannya tiba, baru browser mengecek header Access-Control-Allow-Origin:
- Isinya sama dengan origin halaman, atau
*: JavaScript boleh membaca jawabannya. - Gak ada, atau isinya origin lain: browser menahan jawabannya. Di Chrome,
fetchgagal denganTypeError: Failed to fetch, dan console menulis:
Access to fetch at 'https://api.myapp.com/orders' from origin 'https://evil.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Jadi request-nya sampai ke server dan dijawab. Yang ditahan cuma jawabannya, supaya JavaScript evil.com gak bisa membacanya. Itu jawaban tebakannya. Karena itu CORS bukan pengganti login: server tetap harus mengecek siapa yang meminta.
Preflight: browser bertanya dulu
Request seperti DELETE gak langsung dikirim. Browser bertanya dulu lewat request OPTIONS, namanya preflight:
OPTIONS /orders/7 HTTP/1.1
Host: api.myapp.com
Origin: https://myapp.com
Access-Control-Request-Method: DELETE
Server yang mengizinkan menjawab:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 7200
Kalau origin dan method-nya diizinkan, browser baru mengirim DELETE /orders/7 yang asli. Kalau gak, request aslinya gak pernah dikirim, jadi order #7 aman.
Preflight cuma dilewati untuk request sederhana, yaitu yang memenuhi semua ini:
- method-nya
GET,HEAD, atauPOST; - header-nya cuma yang dianggap aman oleh browser, seperti
AcceptdanContent-Type, tanpaAuthorizationatau header buatan sendiri; Content-Type-nya (kalau ada)application/x-www-form-urlencoded,multipart/form-data, atautext/plain.
Jadi PUT, DELETE, header Authorization, dan Content-Type: application/json semuanya memicu preflight. Access-Control-Max-Age bilang berapa detik jawaban preflight boleh diingat. Chrome membatasinya sampai 7.200 detik (2 jam), Firefox sampai 86.400 detik (24 jam).
Memasang header CORS di server
Server memutuskan per request: origin yang meminta ada di daftar izin (allowlist) atau gak. Ini function yang sama dengan latihan:
function setCORSHeaders(res, origin, allowedOrigins) {
if (allowedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
}
// origin lain: gak dapat header apa pun
}
const headers = {};
const res = { setHeader(name, value) { headers[name] = value; } };
setCORSHeaders(res, "https://myapp.com", ["https://myapp.com", "https://admin.myapp.com"]);
console.log("Access-Control-Allow-Origin:", headers["Access-Control-Allow-Origin"]);
console.log("Access-Control-Allow-Methods:", headers["Access-Control-Allow-Methods"]);
console.log("Access-Control-Allow-Headers:", headers["Access-Control-Allow-Headers"]);
const headers2 = {};
const res2 = { setHeader(name, value) { headers2[name] = value; } };
setCORSHeaders(res2, "https://evil.com", ["https://myapp.com"]);
console.log("Blocked origin headers:", JSON.stringify(headers2));
Output-nya:
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Blocked origin headers: {}
resdi sini objek tiruan response:setHeader(name, value)cuma mencatat header ke objekheaders. Di Express,res.setHeaderyang asli dipanggil dengan cara yang sama.allowedOrigins.includes(origin)mencocokkan origin persis, karakter per karakter.https://myapp.com.evil.comgak lolos.- Origin yang diizinkan dipantulkan ke
Access-Control-Allow-Origin, karena header ini cuma boleh berisi satu origin (atau*). https://evil.comgak ada di daftar, jadiheaders2tetap kosong danJSON.stringify(headers2)mencetak{}.
Credentials: kalau cookie ikut
Secara bawaan, fetch lintas origin gak membawa cookie. Untuk membawanya, halaman menulis fetch(url, { credentials: 'include' }), dan jawaban server harus memenuhi dua syarat:
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Credentials: truewajib ada.Access-Control-Allow-Originharus origin yang persis, bukan*. Kombinasi*dengan credentials ditolak browser dengan pesanThe value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
Jadi Access-Control-Allow-Origin: * bersama Access-Control-Allow-Credentials: true itu konfigurasi yang salah: aplikasimu sendiri gagal membaca jawabannya. Bahaya sebenarnya datang dari "perbaikan" yang keliru, yaitu memantulkan origin apa pun tanpa dicek.
💡 Di Express dan Laravel
Biasanya kamu gak menulis header CORS sendiri. Di Express ada package cors dengan opsi yang mirip (origin, methods, allowedHeaders, credentials, maxAge). Di Laravel, CORS sudah dipasang bawaan. Aturannya ada di config/cors.php (sejak Laravel 11, buat file-nya dengan php artisan config:publish cors):
return [
'paths' => ['api/*'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],
'allowed_origins' => ['https://myapp.com', 'https://admin.myapp.com'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['Content-Type', 'Authorization'],
'exposed_headers' => [],
'max_age' => 7200,
'supports_credentials' => true,
];
Keduanya mencocokkan origin dengan daftar, lalu menambahkan Vary: Origin ke jawaban. Header itu memberi tahu cache seperti CDN bahwa jawabannya tergantung origin, supaya jawaban untuk satu origin gak disajikan ke origin lain.
⚠️ Jebakan yang sering ketemu
- Memantulkan origin apa pun.
res.setHeader("Access-Control-Allow-Origin", req.headers.origin)tanpa cek daftar, apalagi ditambahAccess-Control-Allow-Credentials: true, sama saja mengizinkan semua situs membaca data user yang sedang login. Cek dulu denganallowedOrigins.includes(origin). *bersama credentials. Browser menolaknya. Ganti*dengan origin persis dari allowlist.- Origin yang ditolak tetap diberi header. Kalau origin gak ada di daftar, jangan pasang header CORS apa pun, termasuk Methods dan Headers. Di latihan,
JSON.stringify(headers2)harus{}. - Mengira CORS melindungi server. CORS cuma aturan browser soal siapa yang boleh membaca jawaban.
curl, Postman, dan server lain gak peduli CORS, dan request sederhana tetap sampai. Endpoint-mu tetap butuh login dan cek izin.
TL;DR: CORS (Cross-Origin Resource Sharing) adalah izin dari server agar JavaScript di origin lain boleh membaca jawabannya. Origin = scheme + host + port, jadi https://api.myapp.com beda origin dengan https://myapp.com. Request sederhana (GET) tetap sampai ke server; browser cuma menahan jawabannya kalau Access-Control-Allow-Origin gak cocok. DELETE, PUT, header Authorization atau JSON memicu preflight OPTIONS dulu, dan kalau ditolak, request aslinya gak pernah dikirim. Di server, pantulkan origin hanya kalau allowedOrigins.includes(origin), pasang Methods dan Headers bersamanya, dan biarkan origin lain tanpa header sama sekali. Dengan cookie (credentials: 'include') wajib Access-Control-Allow-Credentials: true dan origin persis, gak boleh *.