Browser mengisolasi situs yang berbeda dengan satu aturan dasar bernama kebijakan same-origin, yaitu kode dari satu origin tidak boleh membaca data milik origin lain. Topik ini membedah anatomi origin, mengujinya melalui pembanding interaktif beserta uji otomatisnya, lalu membahas CORS sebagai relaksasi yang terkendali atas kebijakan tersebut. Setelah selesai membaca, apakah sebuah kombinasi URL dianggap satu origin atau dua origin yang berbeda dapat dinilai secara pasti.
Anatomi sebuah origin#
Origin adalah identitas asal sebuah dokumen yang tersusun atas tiga komponen, yaitu skema, host, dan port. Dua URL dianggap same-origin hanya bila ketiga komponennya sama persis, dan disebut cross-origin bila salah satunya berbeda. Jalur dan query tidak termasuk origin, sehingga https://situs-contoh.test/halaman dan https://situs-contoh.test/lainnya berada pada origin yang sama. Skema http dan https selalu berbeda origin walaupun host dan portnya sama.
Port memerlukan catatan khusus karena memiliki nilai bawaan, yaitu 443 untuk https dan 80 untuk http. Penulisan https://situs-contoh.test/ dan https://situs-contoh.test:443/ menghasilkan origin yang sama karena port 443 adalah nilai bawaannya. Nama host juga tidak peka huruf besar-kecil, sehingga penulisan yang berbeda kapitalnya tetap dianggap origin yang sama. Diagram berikut memerinci satu URL contoh ke dalam komponen-komponennya.
Pembanding origin dan uji otomatisnya#
Demo berikut menyediakan lima URL contoh yang dapat dipasangkan secara bebas. Pilih dua URL, klik tombol Bandingkan, lalu halaman menampilkan vonis SAME-ORIGIN atau CROSS-ORIGIN beserta rincian tiga komponen origin dari masing-masing URL. Pasangan yang hanya berbeda jalur akan selalu menghasilkan SAME-ORIGIN.
Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Pembanding Origin</title>
<style>
body { font-family: system-ui, sans-serif; margin: 16px; background: #f7fafc; color: #1a202c; }
.baris { display: flex; gap: 12px; align-items: center; margin: 10px 0; flex-wrap: wrap; }
select { flex: 1; min-width: 220px; padding: 6px; font-size: 13px; }
button { padding: 8px 16px; font-size: 14px; }
#hasil { margin-top: 14px; padding: 12px; border-radius: 6px; font-weight: 600; min-height: 20px; }
.sama { background: #c6f6d5; color: #22543d; }
.beda { background: #fed7d7; color: #742a2a; }
table { border-collapse: collapse; margin-top: 8px; width: 100%; }
td { border: 1px solid #cbd5e0; padding: 4px 8px; font-size: 13px; font-weight: 400; }
td:first-child { width: 60px; font-weight: 600; }
</style>
</head>
<body>
<p>Pilih dua URL, lalu klik Bandingkan untuk melihat vonisnya:</p>
<div class="baris">
<select id="url-a"></select>
<select id="url-b"></select>
<button id="bandingkan" type="button">Bandingkan</button>
</div>
<div id="hasil" class="">Hasil perbandingan akan tampil di sini.</div>
<script src="index.js"></script>
<script>
// Pembanding origin: berjalan di browser (demo) dan di Node (uji otomatis).
// Pada Node, objek document tidak ada sehingga hanya empat pasangan uji yang dicetak.
const DAFTAR_URL = [
"https://situs-contoh.test/halaman",
"https://situs-contoh.test:8443/",
"http://situs-contoh.test/halaman",
"https://blog.situs-contoh.test/",
"https://situs-contoh.test/lainnya",
];
const PASANGAN_UJI = [
["https://situs-contoh.test/halaman", "https://situs-contoh.test/lainnya"],
["https://situs-contoh.test/halaman", "https://situs-contoh.test:8443/"],
["https://situs-contoh.test/halaman", "http://situs-contoh.test/halaman"],
["https://blog.situs-contoh.test/", "https://situs-contoh.test/halaman"],
];
function portEfektif(url) {
if (url.port) return url.port;
if (url.protocol === "https:") return "443";
if (url.protocol === "http:") return "80";
return "";
}
function originDari(teks) {
const url = new URL(teks);
return { skema: url.protocol.replace(":", ""), host: url.hostname, port: portEfektif(url) };
}
function samaOrigin(urlA, urlB) {
const a = originDari(urlA);
const b = originDari(urlB);
return a.skema === b.skema && a.host === b.host && a.port === b.port;
}
if (typeof document === "undefined") {
for (const [a, b] of PASANGAN_UJI) {
const label = samaOrigin(a, b) ? "SAME-ORIGIN" : "CROSS-ORIGIN";
console.log(label.padEnd(12) + " " + a + " vs " + b);
}
} else {
const pilihA = document.getElementById("url-a");
const pilihB = document.getElementById("url-b");
for (const u of DAFTAR_URL) {
pilihA.add(new Option(u, u));
pilihB.add(new Option(u, u));
}
document.getElementById("bandingkan").addEventListener("click", () => {
const a = originDari(pilihA.value);
const b = originDari(pilihB.value);
const sama = samaOrigin(pilihA.value, pilihB.value);
const hasil = document.getElementById("hasil");
hasil.className = sama ? "sama" : "beda";
const vonis = document.createTextNode(sama ? "SAME-ORIGIN" : "CROSS-ORIGIN");
const tabel = document.createElement("table");
for (const [label, o] of [["URL A", a], ["URL B", b]]) {
const tr = document.createElement("tr");
const td1 = document.createElement("td");
td1.textContent = label;
const td2 = document.createElement("td");
td2.textContent = o.skema + " | " + o.host + " | " + o.port;
tr.append(td1, td2);
tabel.append(tr);
}
hasil.replaceChildren(vonis, tabel);
});
}
</script></body>
</html>Logika pembanding sengaja ditulis sekali untuk dua lingkungan. Di browser, berkas index.js mengisi daftar pilihan dan merender hasilnya, sedangkan di Node, objek document tidak ada sehingga bagian halaman dilewati dan empat pasangan uji dicetak ke layar. Bentuk ganda seperti ini memungkinkan logika yang sama diuji tanpa browser, cukup dengan menjalankan node index.js pada direktori topik. Pemeriksaannya pun murni operasi teks atas URL, sehingga demo berjalan di dalam iframe tanpa satu pun panggilan jaringan.
// Pembanding origin: berjalan di browser (demo) dan di Node (uji otomatis).
// Pada Node, objek document tidak ada sehingga hanya empat pasangan uji yang dicetak.
const DAFTAR_URL = [
"https://situs-contoh.test/halaman",
"https://situs-contoh.test:8443/",
"http://situs-contoh.test/halaman",
"https://blog.situs-contoh.test/",
"https://situs-contoh.test/lainnya",
];
const PASANGAN_UJI = [
["https://situs-contoh.test/halaman", "https://situs-contoh.test/lainnya"],
["https://situs-contoh.test/halaman", "https://situs-contoh.test:8443/"],
["https://situs-contoh.test/halaman", "http://situs-contoh.test/halaman"],
["https://blog.situs-contoh.test/", "https://situs-contoh.test/halaman"],
];
function portEfektif(url) {
if (url.port) return url.port;
if (url.protocol === "https:") return "443";
if (url.protocol === "http:") return "80";
return "";
}
function originDari(teks) {
const url = new URL(teks);
return { skema: url.protocol.replace(":", ""), host: url.hostname, port: portEfektif(url) };
}
function samaOrigin(urlA, urlB) {
const a = originDari(urlA);
const b = originDari(urlB);
return a.skema === b.skema && a.host === b.host && a.port === b.port;
}
if (typeof document === "undefined") {
for (const [a, b] of PASANGAN_UJI) {
const label = samaOrigin(a, b) ? "SAME-ORIGIN" : "CROSS-ORIGIN";
console.log(label.padEnd(12) + " " + a + " vs " + b);
}
} else {
const pilihA = document.getElementById("url-a");
const pilihB = document.getElementById("url-b");
for (const u of DAFTAR_URL) {
pilihA.add(new Option(u, u));
pilihB.add(new Option(u, u));
}
document.getElementById("bandingkan").addEventListener("click", () => {
const a = originDari(pilihA.value);
const b = originDari(pilihB.value);
const sama = samaOrigin(pilihA.value, pilihB.value);
const hasil = document.getElementById("hasil");
hasil.className = sama ? "sama" : "beda";
const vonis = document.createTextNode(sama ? "SAME-ORIGIN" : "CROSS-ORIGIN");
const tabel = document.createElement("table");
for (const [label, o] of [["URL A", a], ["URL B", b]]) {
const tr = document.createElement("tr");
const td1 = document.createElement("td");
td1.textContent = label;
const td2 = document.createElement("td");
td2.textContent = o.skema + " | " + o.host + " | " + o.port;
tr.append(td1, td2);
tabel.append(tr);
}
hasil.replaceChildren(vonis, tabel);
});
}SAME-ORIGIN https://situs-contoh.test/halaman vs https://situs-contoh.test/lainnya
CROSS-ORIGIN https://situs-contoh.test/halaman vs https://situs-contoh.test:8443/
CROSS-ORIGIN https://situs-contoh.test/halaman vs http://situs-contoh.test/halaman
CROSS-ORIGIN https://blog.situs-contoh.test/ vs https://situs-contoh.test/halamanEmpat baris keluaran menutup seluruh aturan pemeriksaan. Jalur yang berbeda tetap same-origin, sedangkan port 8443, skema http, dan subdomain blog masing-masing menghasilkan cross-origin. Fungsi originDari pada berkas tersebut juga memperlihatkan cara memperoleh port efektif, karena properti port pada objek URL kosong bila portnya adalah nilai bawaan.
Yang dijaga dan yang tidak dijaga#
Kebijakan same-origin membatasi pembacaan, bukan penyematan. Skrip dari satu origin tidak boleh membaca respons fetch atau XMLHttpRequest dari origin lain, tidak boleh mengakses DOM dokumen dalam iframe dari origin lain, dan penyimpanan seperti localStorage dipisahkan per origin. Sebaliknya, menyematkan sumber lintas origin tetap diizinkan: tag img, script, dan CSS dapat dimuat dari mana pun, form dapat dikirim ke origin lain, dan pengalihan alamat bebas dilakukan. Polanya konsisten, yaitu penulisan mudah dilakukan sedangkan pembacaan data diblokir.
Batas ini melindungi pengguna karena browser mengirim kredensial secara otomatis. Apabila situs jahat terbuka di tab lain, skripnya bisa saja mencoba membaca isi webmail atau halaman rekening milik pengguna yang sedang login, dan pencegahnya justru kebijakan same-origin yang ditegakkan browser, bukan server situs tersebut. Kebijakan ini tetap berlaku bahkan ketika server tidak melakukan apa pun.
Kebijakan ini juga bukan pertahanan universal. Ia tidak menghalangi server untuk mengambil data dari origin lain sebagaimana terjadi pada SSRF dalam kategori A01 Broken Access Control, tidak memeriksa isi yang disematkan, dan cookie mengikuti aturan lingkup tersendiri yang tidak identik dengan origin. Pemisahan tersebut menjaga kebijakan tetap sederhana, tetapi menuntut lapisan pertahanan lain pada area yang tidak tercakup.
CORS: relaksasi yang terkendali#
Banyak aplikasi sah memang perlu membaca data lintas origin, misalnya frontend pada aplikasi.contoh.test yang memanggil API pada api.contoh.test. CORS (Cross-Origin Resource Sharing) adalah mekanisme header HTTP yang memungkinkan server melonggarkan kebijakan untuk origin tertentu. Browser tetap memblokir pembacaan secara default, lalu menyerahkan keputusannya pada header respons Access-Control-Allow-Origin yang menyebut origin yang diizinkan.
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://aplikasi.contoh.test
Vary: OriginPermintaan tertentu diperiksa lebih dahulu melalui preflight, yaitu permintaan OPTIONS yang dikirim browser sebelum permintaan sesungguhnya. Preflight diperlukan untuk method selain GET, HEAD, dan POST, untuk header kustom, serta untuk beberapa jenis konten. Server menjawab dengan daftar method dan header yang diizinkan, dan permintaan asli baru dikirim bila jawabannya cocok.
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://aplikasi.contoh.test
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-TypeKelonggaran ini mudah berubah menjadi celah bila dikonfigurasi ceroboh. Nilai Access-Control-Allow-Origin yang mencerminkan origin peminta tanpa pemeriksaan daftar putih membuka data privat untuk dibaca origin mana pun, dan pola kesalahan seperti ini termasuk kategori A02 Security Misconfiguration pada OWASP Top 10:2025. Konfigurasi CORS yang benar menyebut origin secara eksplisit dan mengatur header Access-Control-Allow-Credentials hanya bila memang diperlukan.
Sebelum CORS didukung merata, pengembang menyiasati larangan pembacaan dengan JSONP, yaitu teknik yang memanfaatkan kebebasan tag script memuat sumber lintas origin. Respons dibungkus sebagai panggilan fungsi, sehingga data masuk sebagai kode yang langsung dieksekusi. Caranya hanya mengakomodasi GET, menangani error dengan buruk, dan mengeksekusi kode pihak ketiga pada halaman sendiri. CORS kini didukung seluruh browser modern, sehingga JSONP berstatus warisan yang sebaiknya dihapus, bukan ditiru.
Ringkasan#
- Origin tersusun atas skema, host, dan port, dan ketiganya harus identik agar dua URL dianggap same-origin.
- Kebijakan same-origin memblokir pembacaan lintas origin, sedangkan penyematan sumber seperti gambar dan skrip tetap diizinkan.
- CORS melonggarkan kebijakan melalui header respons yang ditentukan server, termasuk pemeriksaan awal preflight.
- Kesalahan konfigurasi CORS mengubah pelindung menjadi pintu masuk sehingga terhitung sebagai A02 Security Misconfiguration.
Referensi#
- Same-origin policy — MDN — Definisi resmi kebijakan same-origin beserta daftar mekanisme yang dibatasi dan yang dikecualikan.
- CORS — MDN — Dokumentasi Cross-Origin Resource Sharing, termasuk skenario prapemeriksaan dan header yang terlibat.