Browser modern menerapkan same-origin policy: JavaScript pada sebuah halaman hanya boleh membaca respons dari origin yang sama dengan halaman tersebut. Kebijakan ini melindungi pengguna dari situs jahat yang mencoba membaca data sensitif lewat permintaan diam-diam. Aplikasi modern — misalnya frontend di https://app.example.com yang memanggil API di https://api.example.com — justru membutuhkan akses lintas origin yang sah. CORS (Cross-Origin Resource Sharing) adalah mekanisme HTTP yang membuka akses tersebut secara terkendali melalui header respons. Pada chapter ini kita akan membedah konsep origin, alasan browser mengirim preflight OPTIONS, daftar header CORS yang benar, lalu praktik menulis middleware CORS di Go dengan httptest.
Origin dan Host: Dua Hal yang Berbeda #
Origin adalah gabungan tiga bagian: scheme, host, dan port. URL https://app.example.com/beranda memiliki origin https://app.example.com; path tidak dihitung, sedangkan scheme, host, dan port harus sama persis. Tabel berikut membandingkan beberapa URL terhadap origin https://app.example.com:
| URL | Origin sama? | Penyebab |
|---|---|---|
https://app.example.com/profil | Ya | Hanya path yang berbeda |
http://app.example.com/beranda | Tidak | Scheme berbeda (http vs https) |
https://api.example.com/beranda | Tidak | Host berbeda |
https://app.example.com:8443/beranda | Tidak | Port berbeda (8443 vs 443 implisit) |
Header Host pada permintaan HTTP menunjukkan server tujuan permintaan, sedangkan header Origin yang dikirim browser menunjukkan halaman asal permintaan. Server membaca header Origin untuk memutuskan apakah JavaScript dari halaman tersebut boleh membaca respons. Oleh karena itu, server membandingkan header Origin terhadap daftar origin yang dipercaya.
Mengapa Browser Mengirim Preflight OPTIONS #
Permintaan “sederhana” seperti GET tanpa header khusus langsung dikirim ke server, lalu keputusan baca/tolak dilakukan setelah respons tiba. Permintaan yang tidak sederhana — misalnya method PUT atau DELETE, header kustom seperti Authorization, atau Content-Type: application/json — terlebih dahulu memicu preflight: browser mengirim request OPTIONS dengan header Access-Control-Request-Method dan Access-Control-Request-Headers sebagai pertanyaan izin sebelum permintaan sungguhan dikirim.
Server menjawab preflight dengan status 204 No Content beserta header Access-Control-Allow-Methods, Access-Control-Allow-Headers, dan Access-Control-Max-Age. Nilai Max-Age dalam detik menentukan berapa lama browser boleh menyimpan jawaban preflight, sehingga preflight tidak dikirim berulang-ulang. Karena preflight bukan bagian logika aplikasi, jawab preflight di middleware tanpa menyentuh database.
Header CORS yang Perlu Disetel #
| Header respons | Fungsi | Contoh nilai |
|---|---|---|
Access-Control-Allow-Origin | Origin mana yang boleh membaca respons | https://app.example.com |
Access-Control-Allow-Methods | Method yang diizinkan untuk origin tersebut | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | Header request yang boleh dikirim | Content-Type, Authorization |
Access-Control-Allow-Credentials | Mengizinkan cookie dikirim lintas origin | true |
Access-Control-Max-Age | Umur cache jawaban preflight (detik) | 600 |
Middleware CORS di Go #
CORS paling rapi diimplementasikan sebagai middleware: function yang membungkus http.Handler dan menyetel header izin sebelum handler berikutnya dipanggil. Program berikut memakai httptest untuk mensimulasikan tiga permintaan tanpa membuka port jaringan: preflight OPTIONS, GET dari origin yang diizinkan, dan GET dari origin asing:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
)
// allowedOrigin adalah satu-satunya origin yang dipercaya server.
const allowedOrigin = "https://app.example.com"
// corsMiddleware menyetel header CORS dan menjawab preflight OPTIONS.
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin == allowedOrigin {
// Nilai spesifik, bukan "*": aman dipakai bersama credentials.
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Add("Vary", "Origin")
}
if r.Method == http.MethodOptions {
// Preflight: browser menanyakan izin sebelum request sungguhan.
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Max-Age", "600")
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// cetakHeader mencetak satu header respons, atau "(tidak diset)" bila kosong.
func cetakHeader(rec *httptest.ResponseRecorder, key string) {
v := rec.Header().Get(key)
if v == "" {
v = "(tidak diset)"
}
fmt.Println(key+":", v)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/salam", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Halo dari API")
})
handler := corsMiddleware(mux)
// Skenario 1: preflight OPTIONS dari origin yang diizinkan.
req := httptest.NewRequest(http.MethodOptions, "/api/salam", nil)
req.Header.Set("Origin", allowedOrigin)
req.Header.Set("Access-Control-Request-Method", "POST")
req.Header.Set("Access-Control-Request-Headers", "Content-Type")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
fmt.Println("== Skenario 1: preflight OPTIONS ==")
fmt.Println("status:", rec.Code)
cetakHeader(rec, "Access-Control-Allow-Origin")
cetakHeader(rec, "Access-Control-Allow-Methods")
cetakHeader(rec, "Access-Control-Allow-Headers")
cetakHeader(rec, "Access-Control-Max-Age")
cetakHeader(rec, "Vary")
// Skenario 2: GET biasa dari origin yang diizinkan.
req = httptest.NewRequest(http.MethodGet, "/api/salam", nil)
req.Header.Set("Origin", allowedOrigin)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
fmt.Println()
fmt.Println("== Skenario 2: GET dari origin diizinkan ==")
fmt.Println("status:", rec.Code)
cetakHeader(rec, "Access-Control-Allow-Origin")
fmt.Println("body:", strings.TrimSpace(rec.Body.String()))
// Skenario 3: GET dari origin asing — header izin tidak diset.
req = httptest.NewRequest(http.MethodGet, "/api/salam", nil)
req.Header.Set("Origin", "https://jahat.example.net")
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
fmt.Println()
fmt.Println("== Skenario 3: GET dari origin asing ==")
fmt.Println("status:", rec.Code)
cetakHeader(rec, "Access-Control-Allow-Origin")
fmt.Println("body tetap terkirim ke browser, namun JavaScript diblokir")
}Output yang Dihasilkan:
== Skenario 1: preflight OPTIONS ==
status: 204
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
Vary: Origin
== Skenario 2: GET dari origin diizinkan ==
status: 200
Access-Control-Allow-Origin: https://app.example.com
body: Halo dari API
== Skenario 3: GET dari origin asing ==
status: 200
Access-Control-Allow-Origin: (tidak diset)
body tetap terkirim ke browser, namun JavaScript diblokirMari kita telaah baris demi baris program di atas:
- Baris 14-16 (
corsMiddleware): function menerima handler berikutnya (next) lalu mengembalikan handler baru; baris 16 membaca headerOrigindari setiap request yang lewat. - Baris 17-21: hanya request dengan
Originyang cocok denganallowedOrigin(baris 11) yang mendapat headerAccess-Control-Allow-Originbernilai spesifik, ditambahVary: Originuntuk cache perantara. - Baris 22-29: preflight
OPTIONSdijawab langsung dengan status204, header izin, lalureturn— permintaan tidak pernah diteruskan kemuxmaupun database. - Baris 45 (
mux.HandleFunc("GET /api/salam", ...)): pattern berbasis method (sejak Go 1.22) mendaftarkan handler API;OPTIONStidak pernah sampai kemuxkarena sudah dicegat middleware. - Baris 51-56 (
httptest.NewRequestdanhttptest.NewRecorder): membuat request dan perekam respons palsu;handler.ServeHTTP(rec, req)menjalankan seluruh rantai middleware tanpa jaringan sungguhan. - Baris 78-88 (skenario 3): origin asing tetap menerima respons
200beserta body, namun header izin tidak diset — yang memblokir akses adalah browser, bukan server.
Skenario 3 pada output di atas menunjukkan sifat CORS yang sering disalahpahami: CORS bukan mekanisme autentikasi. curl atau skrip penyerang tidak menjalankan same-origin policy dan tetap bisa membaca body Halo dari API. Header CORS hanya menginstruksikan browser untuk memblokir JavaScript dari origin asing. Autentikasi dan otorisasi di sisi server tetap wajib, sebagaimana dibahas pada chapter Token-based Authentication
.
Hubungan dengan Cookie SameSite #
Atribut SameSite pada cookie mengatur kapan browser mengirim cookie lintas situs, sedangkan CORS mengatur kapan JavaScript boleh membaca respons lintas origin. Cookie SameSite=None; Secure hanya terbaca pada aplikasi lintas origin bila server juga menyetel Access-Control-Allow-Credentials: true dan origin spesifik. Detail SameSite dibahas pada chapter Cookie Security
dan pencegahan permintaan palsu antarsitus pada chapter CSRF
.
Referensi #
- MDN Web Docs: CORS — referensi resmi mekanisme CORS dari Mozilla.
- Dokumentasi package net/http
— type
http.Handlerdanhttp.HandlerFuncuntuk middleware. - Dokumentasi package httptest
—
NewRequestdanNewRecorderuntuk simulasi request deterministik.
Dengan middleware CORS, Vary: Origin, dan origin spesifik, API kita dapat diakses frontend lintas origin secara aman. Chapter berikutnya membahas celah keamanan di lapisan yang berbeda: SQL injection — serangan yang mengeksploitasi cara kita menyusun teks query.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
cors-demo.go
Unduh berkaspackage main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
)
// allowedOrigin adalah satu-satunya origin yang dipercaya server.
const allowedOrigin = "https://app.example.com"
// corsMiddleware menyetel header CORS dan menjawab preflight OPTIONS.
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin == allowedOrigin {
// Nilai spesifik, bukan "*": aman dipakai bersama credentials.
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Add("Vary", "Origin")
}
if r.Method == http.MethodOptions {
// Preflight: browser menanyakan izin sebelum request sungguhan.
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Max-Age", "600")
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// cetakHeader mencetak satu header respons, atau "(tidak diset)" bila kosong.
func cetakHeader(rec *httptest.ResponseRecorder, key string) {
v := rec.Header().Get(key)
if v == "" {
v = "(tidak diset)"
}
fmt.Println(key+":", v)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/salam", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Halo dari API")
})
handler := corsMiddleware(mux)
// Skenario 1: preflight OPTIONS dari origin yang diizinkan.
req := httptest.NewRequest(http.MethodOptions, "/api/salam", nil)
req.Header.Set("Origin", allowedOrigin)
req.Header.Set("Access-Control-Request-Method", "POST")
req.Header.Set("Access-Control-Request-Headers", "Content-Type")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
fmt.Println("== Skenario 1: preflight OPTIONS ==")
fmt.Println("status:", rec.Code)
cetakHeader(rec, "Access-Control-Allow-Origin")
cetakHeader(rec, "Access-Control-Allow-Methods")
cetakHeader(rec, "Access-Control-Allow-Headers")
cetakHeader(rec, "Access-Control-Max-Age")
cetakHeader(rec, "Vary")
// Skenario 2: GET biasa dari origin yang diizinkan.
req = httptest.NewRequest(http.MethodGet, "/api/salam", nil)
req.Header.Set("Origin", allowedOrigin)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
fmt.Println()
fmt.Println("== Skenario 2: GET dari origin diizinkan ==")
fmt.Println("status:", rec.Code)
cetakHeader(rec, "Access-Control-Allow-Origin")
fmt.Println("body:", strings.TrimSpace(rec.Body.String()))
// Skenario 3: GET dari origin asing — header izin tidak diset.
req = httptest.NewRequest(http.MethodGet, "/api/salam", nil)
req.Header.Set("Origin", "https://jahat.example.net")
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
fmt.Println()
fmt.Println("== Skenario 3: GET dari origin asing ==")
fmt.Println("status:", rec.Code)
cetakHeader(rec, "Access-Control-Allow-Origin")
fmt.Println("body tetap terkirim ke browser, namun JavaScript diblokir")
}