Salah konfigurasi CORS (CWE-942): bagaimana pemantulan Origin membocorkan data dan cara memperbaikinya
API yang menjawab Origin apa pun dengan Access-Control-Allow-Origin bernilai origin itu dan Access-Control-Allow-Credentials: true membiarkan setiap situs membaca responsnya dengan cookie pengunjung. Bagaimana pemeriksaan akhiran, origin null, dan origin yang dipantulkan merusak kebijakan same-origin, serta allowlist dengan kecocokan persis yang memperbaikinya, dengan contoh Express dan cors.
Penjelasan Sederhana (ELI5)
Seorang teller bank akan membacakan saldo Anda kepada siapa pun yang Anda utus, asalkan utusan itu menunjukkan surat yang menyebut untuk siapa ia bekerja dan daftar milik teller menyetujui nama itu. Aturan seorang teller adalah 'setujui nama apa pun yang berakhiran kata Santoso'. Orang asing mengirim utusan dengan surat dari 'Budi Santoso', teller menyetujuinya, dan saldo Anda dibacakan kepada orang yang tidak pernah Anda utus. Pemantulan Origin adalah versi malasnya, ketika teller menyetujui semua nama. Perbaikannya adalah daftar nama yang pendek dan persis, dicocokkan huruf demi huruf.
Konsep Kunci & Istilah
- Kebijakan same-origin
- Browser hanya membiarkan skrip membaca respons dari origin-nya sendiri (skema, host, dan port). Header respons CORS adalah cara server sengaja melonggarkan aturan itu untuk origin tertentu.
- Access-Control-Allow-Origin dengan kredensial
- Jika respons menyebut origin pemanggil di
Access-Control-Allow-Origindan mengirimAccess-Control-Allow-Credentials: true, skrip di origin itu bisa membaca respons dari request yang membawa cookie pengguna. Browser menolak*bersama kredensial, karena itulah server yang salah konfigurasi memantulkan origin. - Pemantulan Origin dan pencocokan akhiran
- Menyalin header
Originke respons berarti memercayai semua situs. Pemeriksaan sepertiendsWith('example.com'), pencocokan substring, atau regex tanpa jangkar dengan titik yang tidak di-escape juga menerimahttps://attacker-example.com. - Origin null
- Iframe bersandbox, URL
data:, dan sebagian redirect mengirimOrigin: null. Penyerang mana pun bisa menghasilkannya, jadi mengizinkannullsama dengan mengizinkan semua orang. - Vary: Origin
- Ketika
Access-Control-Allow-Originbergantung pada request, respons harus membawaVary: Originagar cache bersama tidak menyajikan header CORS milik satu origin ke origin lain. Paketcorsmenambahkannya untuk origin dinamis.
Alur Serangan Langkah demi Langkah
Korban login di situs API
Browser-nya menyimpan cookie sesi untuk api.example.com yang ikut dikirim pada request lintas situs.
Korban membuka halaman di domain penyerang
Sebuah skrip memanggil fetch('https://api.example.com/api/account', { credentials: 'include' }). Halaman di https://attacker-example.com yang hanya mencetak panjang respons ke console menggambarkan kasus ini.
API menyetujui origin itu
Pemeriksaan akhiran menerima attacker-example.com, sehingga respons membawa origin itu di Access-Control-Allow-Origin bersama Access-Control-Allow-Credentials: true.
Skrip membaca data pribadi
Browser menyerahkan JSON akun ke skrip penyerang, yang bisa mengirimnya ke mana saja. Pada 2016 James Kettle menemukan pola ini di beberapa bursa Bitcoin, tempat pola itu mengekspos API key dan data dompet.
Kode Sumber: Rentan vs Aman
// server.js: origin apa pun yang berakhiran example.com, ditambah null, boleh membaca dengan cookie
import express from 'express';
import cors from 'cors';
import { accountFor, requireSession } from './account.js';
const app = express();
// true membuat cors memantulkan Origin pemanggil bersama kredensial
app.use(cors({
origin: (origin, callback) => callback(null, !origin || origin === 'null' || origin.endsWith('example.com')),
credentials: true,
}));
// data akun pribadi yang bisa dibaca origin yang disetujui
app.get('/api/account', requireSession, (req, res) => {
res.json(accountFor(req.session.userId));
});
// server.js: dua origin persis, tidak ada yang lain
import express from 'express';
import cors from 'cors';
import { accountFor, requireSession } from './account.js';
const app = express();
// origin lengkap dengan skema dan host, dibandingkan persis
const ALLOWED_ORIGINS = new Set(['https://app.example.com', 'https://admin.example.com']);
// Origin yang tidak dikenal atau kosong tidak mendapat header CORS; cors menambahkan Vary: Origin
app.use(cors({
origin: (origin, callback) => callback(null, origin !== undefined && ALLOWED_ORIGINS.has(origin)),
credentials: true,
methods: ['GET', 'POST'],
maxAge: 600,
}));
// data pribadi tidak pernah disimpan cache bersama
app.get('/api/account', requireSession, (req, res) => {
res.set('Cache-Control', 'no-store');
res.json(accountFor(req.session.userId));
});
Daftar Periksa Penguatan Sistem Rekayasa
- Simpan allowlist origin lengkap (skema, host, dan port) dengan kecocokan persis dan bandingkan lewat pencarian
Set; jangan pernah memantulkanOriginatau memakai pemeriksaan akhiran, substring, atau regex tanpa jangkar. - Jangan pernah mengizinkan origin
null, dan jangan pernah mengizinkan kredensial untuk origin yang tidak Anda kendalikan. - Kirim
Vary: Originsetiap kaliAccess-Control-Allow-Origindipilih per request, danCache-Control: no-storepada respons berisi data pribadi. - Setel cookie sesi ke
SameSite=LaxatauStrictagar request lintas situs tidak membawanya. - Tambahkan tes CI yang mengirim
Origin: https://attacker.exampledanOrigin: nullke route yang butuh login dan gagal jikaAccess-Control-Allow-Originmuncul kembali.