1. Pengenalan Same-Origin Policy & CORS
Cross-Origin Resource Sharing (CORS) adalah mekanisme keamanan browser yang memungkinkan web application di satu origin mengakses resource di origin lain secara terkendali. CORS dibangun di atas Same-Origin Policy (SOP) β fondasi keamanan browser yang membatasi bagaimana dokumen atau script dari satu origin dapat berinteraksi dengan resource dari origin lain.
Apa itu Same-Origin Policy?
Sebuah origin terdiri dari tiga komponen: protocol (http/https), hostname, dan port. Dua URL dianggap "same-origin" hanya jika ketiga komponen tersebut identik.
| URL A | URL B | Same Origin? | Alasan |
|---|---|---|---|
| https://app.com/page1 | https://app.com/page2 | β Ya | Protocol, host, port sama |
| https://app.com | https://app.com:443 | β Ya | Port 443 = default HTTPS |
| https://app.com | http://app.com | β Tidak | Protocol berbeda (https vs http) |
| https://app.com | https://api.app.com | β Tidak | Hostname berbeda (subdomain) |
| https://app.com | https://app.com:8080 | β Tidak | Port berbeda |
| https://app.com | https://evil.com | β Tidak | Hostname berbeda |
Mengapa CORS Diperlukan?
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β SAME-ORIGIN POLICY & CORS β β β β Tanpa CORS (SOP murni): β β ββββββββββββββ β Diblokir ββββββββββββββ β β β app.com βββββββββββββββββΆβ api.com β β β β (Frontend) β Browser β (Backend) β β β ββββββββββββββ menolak ββββββββββββββ β β β β Dengan CORS: β β ββββββββββββββ β Diizinkan ββββββββββββββ β β β app.com β (jika server β api.com β β β β (Frontend) βββββββββββββββββΆβ (Backend) β β β ββββββββββββββ mengizinkan) ββββββββββββββ β β β β Kenapa SOP ada? β β - Mencegah situs jahat membaca data dari bank.com β β - Mencegah CSRF (sederhana) β β - Mencegah pencurian data dari tab lain β β β β Kenapa CORS diperlukan? β β - Modern apps sering terpisah: frontend + API backend β β - SPA (React/Vue/Angular) sering di origin berbeda β β - Microservices perlu komunikasi cross-origin β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. Cara Kerja CORS
CORS menggunakan HTTP headers untuk memberitahu browser apakah origin tertentu diizinkan mengakses resource. Ada dua jenis request CORS: Simple Requests dan Preflighted Requests.
HTTP Headers CORS
| Header | Arah | Deskripsi |
|---|---|---|
Origin | Request (BrowserβServer) | Origin dari mana request berasal |
Access-Control-Allow-Origin | Response (ServerβBrowser) | Origin mana yang diizinkan mengakses resource |
Access-Control-Allow-Methods | Response | HTTP methods yang diizinkan (GET, POST, PUT, DELETE, dll) |
Access-Control-Allow-Headers | Response | Custom headers yang diizinkan dalam request |
Access-Control-Allow-Credentials | Response | Apakah request dengan credentials (cookie, auth header) diizinkan |
Access-Control-Expose-Headers | Response | Response headers mana yang bisa dibaca oleh JavaScript |
Access-Control-Max-Age | Response | Berapa lama hasil preflight bisa di-cache (dalam detik) |
Access-Control-Request-Method | Preflight Request | Method yang akan digunakan dalam request sesungguhnya |
Access-Control-Request-Headers | Preflight Request | Headers yang akan dikirim dalam request sesungguhnya |
Simple Requests
Simple request adalah request yang memenuhi SEMUA kondisi berikut β browser langsung mengirimkannya tanpa preflight:
- Method:
GET,HEAD, atauPOST - Hanya menggunakan headers yang "aman":
Accept,Accept-Language,Content-Language,Content-Type - Content-Type hanya:
application/x-www-form-urlencoded,multipart/form-data, atautext/plain
# Client (https://app.com) mengirim request ke API GET /api/users HTTP/1.1 Host: api.example.com Origin: https://app.com Accept: application/json # Server meresponse dengan CORS headers HTTP/1.1 200 OK Access-Control-Allow-Origin: https://app.com Content-Type: application/json # Browser memeriksa: apakah Origin diizinkan? # β Jika Access-Control-Allow-Origin cocok β response diteruskan ke JS # β Jika tidak cocok β browser memblokir, JS tidak bisa baca response # Contoh Blokir: # Server mengirim: Access-Control-Allow-Origin: https://other.com # Browser: Origin https://app.com TIDAK cocok β BLOKIR! # JS menerima error: "has been blocked by CORS policy" # ============================================= # POST simple request (form-like) # ============================================= POST /api/comments HTTP/1.1 Host: api.example.com Origin: https://app.com Content-Type: application/x-www-form-urlencoded text=Halo+dunia&author=John # Server response: HTTP/1.1 201 Created Access-Control-Allow-Origin: https://app.com Content-Type: application/json
3. Preflight Requests
Ketika request bukan simple request (misalnya menggunakan method PUT/DELETE, custom headers, atau Content-Type application/json), browser akan mengirim preflight request terlebih dahulu menggunakan method OPTIONS. Preflight ini bertanya ke server: "Apakah kamu mengizinkan request dari origin ini dengan method dan headers ini?"
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PREFLIGHT CORS FLOW β
β β
β Browser (https://app.com) β
β β β
β β 1. PREFLIGHT (OPTIONS) β
β β ββββββββββββββββββββββββββββββββββββββββ β
β β β OPTIONS /api/users HTTP/1.1 β β
β β β Origin: https://app.com β β
β β β Access-Control-Request-Method: PUT β β
β β β Access-Control-Request-Headers: β β
β β β Content-Type, Authorization β β
β β βββββββββββββββββ¬βββββββββββββββββββββββ β
β β β β
β β βΌ β
β β ββββββββββββββββ β
β β β Server β β
β β β api.example β β
β β β .com β β
β β ββββββββ¬ββββββββ β
β β β β
β β 2. PREFLIGHT RESPONSE β
β β ββββββββββββββββββββββββββββββββββββββββ β
β β β HTTP/1.1 204 No Content β β
β β β Access-Control-Allow-Origin: β β
β β β https://app.com β β
β β β Access-Control-Allow-Methods: β β
β β β GET, POST, PUT, DELETE β β
β β β Access-Control-Allow-Headers: β β
β β β Content-Type, Authorization β β
β β β Access-Control-Max-Age: 86400 β β
β β ββββββββββββββββββββββββββββββββββββββββ β
β β β
β β 3. ACTUAL REQUEST (jika preflight OK) β
β β ββββββββββββββββββββββββββββββββββββββββ β
β β β PUT /api/users/123 HTTP/1.1 β β
β β β Origin: https://app.com β β
β β β Content-Type: application/json β β
β β β Authorization: Bearer eyJhb... β β
β β β {"name": "John"} β β
β β βββββββββββββββββ¬βββββββββββββββββββββββ β
β β β β
β β 4. ACTUAL RESPONSE β
β β ββββββββββββββββββββββββββββββββββββββββ β
β β β HTTP/1.1 200 OK β β
β β β Access-Control-Allow-Origin: β β
β β β https://app.com β β
β β β {"id": 123, "name": "John"} β β
β β ββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β JavaScript menerima data β
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Kapan Preflight Terjadi?
| Kondisi | Preflight? | Alasan |
|---|---|---|
| GET/HEAD/POST dengan headers standar | Tidak | Simple request |
| PUT / DELETE / PATCH | Ya | Bukan method simple |
| POST dengan Content-Type: application/json | Ya | Bukan simple Content-Type |
| Custom header (Authorization, X-API-Key) | Ya | Bukan simple header |
| Request dengan credentials | Ya (jika bukan simple) | Credentials mempengaruhi caching |
// β Simple request β TIDAK preflight
fetch('https://api.example.com/users', {
method: 'GET'
});
// β Simple request β TIDAK preflight
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'name=John'
});
// β
Preflight β Content-Type application/json
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // Bukan simple Content-Type!
},
body: JSON.stringify({ name: 'John' })
});
// Browser akan mengirim OPTIONS terlebih dahulu!
// β
Preflight β Custom header Authorization
fetch('https://api.example.com/users', {
method: 'GET',
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiJ9...' // Custom header!
}
});
// β
Preflight β Method PUT
fetch('https://api.example.com/users/123', {
method: 'PUT', // Bukan simple method!
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Jane' })
});
4. Credentials & Cookies
Secara default, CORS request tidak mengirimkan credentials (cookies, HTTP authentication, client certificates). Untuk mengirim credentials, client harus secara eksplisit menyetel credentials: 'include', dan server harus meresponse dengan Access-Control-Allow-Credentials: true.
- JANGAN gunakan
Access-Control-Allow-Origin: *dengan credentials β browser akan memblokir - Server HARUS mengeksplisitkan origin (misalnya
https://app.com), bukan wildcard - Server HARUS mengirim
Access-Control-Allow-Credentials: true - Client HARUS menyetel
credentials: 'include'pada fetch atauwithCredentials: truepada XHR
// Client-side: Mengirim request dengan cookies
// β
Fetch API dengan credentials
fetch('https://api.example.com/profile', {
method: 'GET',
credentials: 'include', // Kirim cookies!
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => console.log('Profile:', data))
.catch(err => console.error('CORS Error:', err));
// β
XMLHttpRequest dengan credentials
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/profile', true);
xhr.withCredentials = true; // Kirim cookies!
xhr.onload = function() {
if (xhr.status === 200) {
console.log('Profile:', JSON.parse(xhr.responseText));
}
};
xhr.send();
// β
Axios dengan credentials (default)
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.example.com',
withCredentials: true // Kirim cookies ke semua request
});
const profile = await api.get('/profile');
# Preflight request dengan credentials
OPTIONS /api/profile HTTP/1.1
Host: api.example.com
Origin: https://app.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type
# Preflight response β HARUS origin eksplisit, bukan *
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
# Actual request dengan cookie
GET /api/profile HTTP/1.1
Host: api.example.com
Origin: https://app.com
Cookie: session_id=abc123xyz
Content-Type: application/json
# Response dengan CORS headers
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https.com
Access-Control-Allow-Credentials: true
Content-Type: application/json
Set-Cookie: session_id=abc123xyz; SameSite=None; Secure; HttpOnly
{"id": 1, "name": "John Doe", "email": "john@app.com"}
5. CORS Misconfiguration & Serangan
Kesalahan konfigurasi CORS adalah kerentanan keamanan yang sangat umum dan sering dieksploitasi. Konfigurasi yang terlalu permisif bisa memungkinkan situs jahat membaca data sensitif pengguna dari API Anda.
Jenis CORS Misconfiguration
| Misconfiguration | Risiko | Contoh |
|---|---|---|
Wildcard origin * dengan credentials | Diblokir browser, tapi menunjukkan kecerobohan | Access-Control-Allow-Origin: * |
| Reflect origin tanpa validasi | Situs manapun bisa mengakses data korban | Allow-Origin: {request.origin} |
| Origin dengan null value | iframe/sandboxed pages bisa mengakses | Access-Control-Allow-Origin: null |
| Regex yang terlalu luas | Subdomain jahat bisa menembus | Matching *app.com termasuk evilapp.com |
| Trusted subdomain berbahaya | XSS di subdomain = akses ke API utama | Allow semua subdomain termasuk yang user-generated |
Contoh Serangan: Origin Reflection
# β SANGAT BERBAHAYA: Reflect origin tanpa validasi
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.after_request
def add_cors_headers(response):
origin = request.headers.get('Origin')
# β Merefleksikan origin dari request tanpa validasi!
# Artinya SIAPAPUN yang mengirim request akan diizinkan
if origin:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
@app.route('/api/profile')
def profile():
if 'user_id' not in session:
return jsonify({"error": "Unauthorized"}), 401
user = get_user(session['user_id'])
return jsonify({
"name": user.name,
"email": user.email,
"balance": user.balance, # Data sensitif!
"ssn": user.ssn # Sangat sensitif!
})
# Serangan: Situs jahat bisa membaca data korban
# Script di evil.com:
# fetch('https://api.example.com/api/profile', {
# credentials: 'include'
# }).then(r => r.json()).then(data => {
# // Data sensitif korban berhasil dicuri!
# sendToAttacker(data);
# });
# Karena server merefleksikan origin evil.com,
# browser mengizinkan response diteruskan ke JS!
Contoh Serangan: Regex yang Terlalu Luas
# β RENTAN: Regex yang tidak tepat
import re
ALLOWED_ORIGINS_PATTERN = r'https://.*\.example\.com$'
def is_origin_allowed(origin):
return bool(re.match(ALLOWED_ORIGINS_PATTERN, origin))
# Masalah: Regex ini cocok dengan:
# β
https://app.example.com β benar
# β
https://api.example.com β benar
# β https://evil.example.com.attacker.com β COCOK JUGA!
# β https://example.com.attacker.com β COCOK JUGA!
# Karena .* bisa match apapun, dan $ hanya match di akhir
# β
SOLUSI: Gunakan whitelist eksplisit
ALLOWED_ORIGINS = {
'https://app.example.com',
'https://admin.example.com',
'https://api.example.com'
}
def is_origin_allowed(origin):
return origin in ALLOWED_ORIGINS
# Atau regex yang lebih ketat:
STRICT_PATTERN = r'^https://(app|admin|api)\.example\.com$'
def is_origin_allowed_strict(origin):
return bool(re.match(STRICT_PATTERN, origin))
Contoh Serangan: Null Origin
# β RENTAN: Mengizinkan null origin
@app.after_request
def add_cors(response):
origin = request.headers.get('Origin', 'null')
# β Mengizinkan 'null' origin!
# Ini memungkinkan sandboxed iframe mengakses API
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
# Serangan: Penyerang membuat halaman dengan sandboxed iframe
# <iframe sandbox="allow-scripts" src="data:text/html,
# <script>
# fetch('https://api.example.com/api/profile', {
# credentials: 'include'
# }).then(r => r.json()).then(data => {
# parent.postMessage(data, '*');
# });
# </script>
# "></iframe>
#
# Sandbox iframe memiliki origin "null" β dan server mengizinkannya!
# β
SOLUSI: Jangan izinkan null origin
@app.after_request
def add_cors_safe(response):
origin = request.headers.get('Origin')
if origin and origin != 'null' and origin in ALLOWED_ORIGINS:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
6. Implementasi CORS yang Aman
Flask dengan flask-cors
# β
CORS yang aman untuk Flask
from flask import Flask, jsonify
from flask_cors import CORS
import os
app = Flask(__name__)
# β
Opsi 1: Whitelist origin spesifik
ALLOWED_ORIGINS = [
'https://beebanelabs.pages.dev',
'https://admin.beebanelabs.pages.dev',
'http://localhost:3000' # Development only
]
CORS(app, resources={
r"/api/*": {
"origins": ALLOWED_ORIGINS,
"methods": ["GET", "POST", "PUT", "DELETE"],
"allow_headers": ["Content-Type", "Authorization", "X-Requested-With"],
"expose_headers": ["X-Total-Count", "X-Page-Count"],
"supports_credentials": True,
"max_age": 3600 # Cache preflight selama 1 jam
}
})
# β
Opsi 2: Dynamic origin validation
from flask import request
import re
TRUSTED_DOMAINS = re.compile(r'^https://(app|admin|api)\.beebanelabs\.pages\.dev$')
def origin_allowed(origin):
if not origin:
return False
if origin == 'http://localhost:3000' and app.debug:
return True
return bool(TRUSTED_DOMAINS.match(origin))
@app.after_request
def set_cors_headers(response):
origin = request.headers.get('Origin')
if origin and origin_allowed(origin):
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-Requested-With'
response.headers['Access-Control-Expose-Headers'] = 'X-Total-Count'
response.headers['Access-Control-Max-Age'] = '3600'
return response
# Handle preflight OPTIONS request
@app.route('/api/', methods=['OPTIONS'])
def handle_preflight(path):
return '', 204
# Contoh API endpoint
@app.route('/api/users')
def get_users():
return jsonify({"users": []})
@app.route('/api/users/', methods=['PUT'])
def update_user(user_id):
return jsonify({"message": "User updated"})
Express.js CORS
// β
CORS yang aman untuk Express.js
const express = require('express');
const cors = require('cors');
const app = express();
// β
Opsi 1: Whitelist origin
const allowedOrigins = [
'https://beebanelabs.pages.dev',
'https://admin.beebanelabs.pages.dev',
'http://localhost:3000'
];
const corsOptions = {
origin: function (origin, callback) {
// Izinkan request tanpa origin (mobile apps, Postman)
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Origin tidak diizinkan oleh CORS policy'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
credentials: true, // Izinkan cookies
maxAge: 3600, // Cache preflight 1 jam
optionsSuccessStatus: 204
};
app.use('/api', cors(corsOptions));
// β
Opsi 2: Per-route CORS (lebih granular)
app.get('/api/public/data', cors(), (req, res) => {
// Public endpoint β CORS terbuka
res.json({ data: 'public' });
});
app.get('/api/private/profile', cors(corsOptions), (req, res) => {
// Private endpoint β CORS ketat
res.json({ profile: {} });
});
// β
Handle preflight untuk semua route
app.options('*', cors(corsOptions));
// Error handler untuk CORS rejection
app.use((err, req, res, next) => {
if (err.message.includes('CORS')) {
res.status(403).json({
error: 'CORS policy violation',
message: 'Origin tidak diizinkan mengakses resource ini'
});
} else {
next(err);
}
});
app.listen(3000);
Nginx CORS Configuration
# /etc/nginx/conf.d/cors.conf
# Map untuk validasi origin
map $http_origin $cors_origin {
default "";
"https://beebanelabs.pages.dev" $http_origin;
"https://admin.beebanelabs.pages.dev" $http_origin;
"~^https://(app|dev)\.beebanelabs\.pages\.dev$" $http_origin;
}
server {
listen 443 ssl;
server_name api.beebanelabs.pages.dev;
location /api/ {
# CORS headers β hanya jika origin valid
if ($cors_origin != "") {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" always;
add_header Access-Control-Expose-Headers "X-Total-Count" always;
add_header Access-Control-Max-Age 3600 always;
}
# Handle preflight
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin $cors_origin;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With";
add_header Access-Control-Max-Age 3600;
add_header Content-Length 0;
return 204;
}
proxy_pass http://backend;
}
}
7. Best Practices CORS
- β
Whitelist origin eksplisit β jangan gunakan wildcard
*untuk API dengan data sensitif - β Validasi origin dengan tepat β gunakan exact match atau regex yang ketat
- β Jangan izinkan null origin β blokir semua request dengan Origin: null
- β Minimalisir allowed methods β hanya izinkan method yang benar-benar dibutuhkan
- β Minimalisir allowed headers β hanya izinkan headers yang diperlukan
- β Set max-age yang wajar β cache preflight untuk mengurangi overhead (1-24 jam)
- β Hati-hati dengan credentials β hanya izinkan untuk origin yang sangat dipercaya
- β Expose hanya yang perlu β jangan expose internal headers
- β Log dan monitor CORS rejection untuk mendeteksi serangan
- β Jangan mengandalkan CORS sebagai satu-satunya pertahanan β tetap implementasikan autentikasi dan otorisasi yang proper
- CORS BUKAN pertahanan untuk CSRF β CORS hanya membatasi apakah JS bisa membaca response, tapi browser tetap mengirim request
- CORS BUKAN firewall β request tetap sampai ke server, hanya response yang diblokir browser
- CORS tidak melindungi server β hanya melindungi client (browser)
- Non-browser clients (curl, Postman) mengabaikan CORS β CORS hanya berlaku di browser
- CORS bukan pengganti autentikasi β tetap harus ada autentikasi dan otorisasi di server
8. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang CORS Security: