Keamanan

CORS Security: Cross-Origin Resource Sharing

Pelajari mekanisme Same-Origin Policy, cara kerja CORS termasuk preflight requests, simple requests, dan credentials handling β€” beserta konfigurasi CORS yang aman untuk berbagai framework backend

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/page1https://app.com/page2βœ… YaProtocol, host, port sama
https://app.comhttps://app.com:443βœ… YaPort 443 = default HTTPS
https://app.comhttp://app.com❌ TidakProtocol berbeda (https vs http)
https://app.comhttps://api.app.com❌ TidakHostname berbeda (subdomain)
https://app.comhttps://app.com:8080❌ TidakPort berbeda
https://app.comhttps://evil.com❌ TidakHostname berbeda

Mengapa CORS Diperlukan?

Diagram: Same-Origin Policy vs CORS
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚          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
OriginRequest (Browser→Server)Origin dari mana request berasal
Access-Control-Allow-OriginResponse (Server→Browser)Origin mana yang diizinkan mengakses resource
Access-Control-Allow-MethodsResponseHTTP methods yang diizinkan (GET, POST, PUT, DELETE, dll)
Access-Control-Allow-HeadersResponseCustom headers yang diizinkan dalam request
Access-Control-Allow-CredentialsResponseApakah request dengan credentials (cookie, auth header) diizinkan
Access-Control-Expose-HeadersResponseResponse headers mana yang bisa dibaca oleh JavaScript
Access-Control-Max-AgeResponseBerapa lama hasil preflight bisa di-cache (dalam detik)
Access-Control-Request-MethodPreflight RequestMethod yang akan digunakan dalam request sesungguhnya
Access-Control-Request-HeadersPreflight RequestHeaders yang akan dikirim dalam request sesungguhnya

Simple Requests

Simple request adalah request yang memenuhi SEMUA kondisi berikut β€” browser langsung mengirimkannya tanpa preflight:

HTTP β€” Simple CORS Request Flow
# 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?"

Diagram: Preflight CORS Flow
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            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 standarTidakSimple request
PUT / DELETE / PATCHYaBukan method simple
POST dengan Content-Type: application/jsonYaBukan simple Content-Type
Custom header (Authorization, X-API-Key)YaBukan simple header
Request dengan credentialsYa (jika bukan simple)Credentials mempengaruhi caching
JavaScript β€” Trigger Preflight dengan Fetch API
// ❌ 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.

⚠️ Aturan Penting Credentials + CORS
  • 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 atau withCredentials: true pada XHR
JavaScript β€” CORS dengan Credentials
// 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');
HTTP β€” CORS dengan Credentials Flow
# 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 credentialsDiblokir browser, tapi menunjukkan kecerobohanAccess-Control-Allow-Origin: *
Reflect origin tanpa validasiSitus manapun bisa mengakses data korbanAllow-Origin: {request.origin}
Origin dengan null valueiframe/sandboxed pages bisa mengaksesAccess-Control-Allow-Origin: null
Regex yang terlalu luasSubdomain jahat bisa menembusMatching *app.com termasuk evilapp.com
Trusted subdomain berbahayaXSS di subdomain = akses ke API utamaAllow semua subdomain termasuk yang user-generated

Contoh Serangan: Origin Reflection

Python β€” CORS Misconfiguration: 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

Python β€” Regex CORS Bypass
# ❌ 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

Python β€” Null Origin Attack
# ❌ 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

Python Flask β€” CORS Configuration yang Aman
# βœ… 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

JavaScript Express β€” CORS Configuration
// βœ… 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

Nginx β€” CORS Headers
# /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

πŸ’‘ CORS Security Checklist
  • βœ… 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
⚠️ Mitigasi Salah tentang CORS
  • 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:

Pertanyaan 1: Tiga komponen apa yang menentukan "origin" di browser?

a) Protocol, hostname, dan port
b) Domain, path, dan query string
c) IP address, port, dan path
d) Protocol, IP, dan hostname

Pertanyaan 2: Kapan browser mengirim preflight request?

a) Untuk semua request HTTP
b) Saat request bukan simple request (custom headers, PUT/DELETE, application/json)
c) Hanya untuk request GET
d) Hanya saat menggunakan VPN

Pertanyaan 3: Mengapa tidak bisa menggunakan Access-Control-Allow-Origin: * dengan credentials?

a) Karena wildcard terlalu lambat
b) Karena browser secara eksplisit memblokir kombinasi wildcard + credentials untuk keamanan
c) Karena server tidak mendukung wildcard
d) Karena wildcard hanya berfungsi di localhost

Pertanyaan 4: Apa risiko dari origin reflection (server merefleksikan Origin header tanpa validasi)?

a) Server menjadi lambat
b) Situs manapun bisa membaca data sensitif pengguna yang terotentikasi
c) Browser crash
d) Tidak ada risiko, ini fitur browser

Pertanyaan 5: CORS melindungi resource dari akses oleh non-browser clients seperti curl atau Postman?

a) Ya, CORS melindungi dari semua client
b) Ya, tapi hanya di production
c) Tidak, CORS hanya berlaku di browser
d) Tidak, CORS sudah tidak berlaku
πŸ” Zoom
100%
🎨 Tema