Kodeloka
  1. Kodeloka
  2. Kelas Keamanan Aplikasi
  3. Materi

5 · Rantai Pasok & API · Modul 8 dari 8

Keamanan API, CSRF & SSRF

Tiga kelas serangan yang mengeksploitasi kepercayaan: kepercayaan pada peramban, pada asal permintaan, dan pada URL.

CSRF — Menumpang Kredensial Korban

Peramban melampirkan cookie secara otomatis pada setiap permintaan ke sebuah domain, termasuk permintaan yang dipicu dari situs lain. Situs penyerang dapat menyusun formulir yang mengirim ke bank Anda, dan cookie sesi ikut terkirim.

<!-- Di situs penyerang -->
<form action="https://bank.id/transfer" method="POST">
    <input name="ke" value="penyerang">
    <input name="jumlah" value="10000000">
</form>
<script>document.forms[0].submit()</script>

Pertahanan Berlapis untuk CSRF

Set-Cookie: sesi=abc; SameSite=Lax; Secure; HttpOnly
// Token CSRF — pola kirim ganda
const token = randomBytes(32).toString("hex");
res.cookie("csrf", token, { sameSite: "lax", secure: true });
// Klien menyalin nilai itu ke header X-CSRF-Token

// Server membandingkan dengan waktu tetap
if (!timingSafeEqual(Buffer.from(req.cookies.csrf),
                     Buffer.from(req.get("X-CSRF-Token") ?? ""))) {
    return res.status(403).end();
}
💡 API yang memakai token Authorization umumnya kebal CSRF

Serangan CSRF bergantung pada kredensial yang dilampirkan peramban secara otomatis. Header Authorization harus ditambahkan JavaScript secara eksplisit, dan skrip di situs penyerang tidak dapat membaca token milik domain lain. Karena itu API murni berbasis token tidak memerlukan token CSRF — tetapi API yang menerima autentikasi cookie tetap memerlukannya.

⚠️ SameSite=Lax tidak melindungi permintaan GET yang mengubah keadaan

Nilai Lax tetap mengirim cookie pada navigasi tingkat atas dengan metode GET. Bila ada endpoint semacam GET /akun/hapus?id=5, tautan sederhana dari situs mana pun sudah cukup untuk memicunya. Metode GET tidak boleh mengubah keadaan — ini bukan sekadar kerapian REST, melainkan batas keamanan.

CORS Sering Disalahpahami

// Berbahaya — memantulkan asal apa pun sambil mengizinkan kredensial
res.header("Access-Control-Allow-Origin", req.headers.origin);
res.header("Access-Control-Allow-Credentials", "true");

// Aman — daftar putih eksplisit
const ASAL_BOLEH = new Set(["https://app.contoh.id", "https://admin.contoh.id"]);
const asal = req.headers.origin;
if (ASAL_BOLEH.has(asal)) {
    res.header("Access-Control-Allow-Origin", asal);
    res.header("Access-Control-Allow-Credentials", "true");
    res.header("Vary", "Origin");
}
🚫 Memantulkan Origin dengan kredensial sama dengan membuka API ke seluruh internet

Konfigurasi ini sering muncul sebagai "perbaikan cepat" ketika CORS menghalangi pengembangan. Akibatnya, situs mana pun yang dikunjungi pengguna yang sedang login dapat memanggil API Anda dengan cookie mereka dan membaca balasannya. Perhatikan juga Vary: Origin — tanpa itu, cache dapat menyajikan header CORS milik satu asal kepada asal lain.

💡 CORS melindungi pengguna, bukan API Anda

CORS adalah aturan yang ditegakkan peramban tentang siapa yang boleh membaca balasan. Ia tidak menghalangi permintaan dari curl, skrip server, atau alat apa pun di luar peramban. Melonggarkan CORS tidak "membuka" API yang tadinya tertutup — API selalu dapat dipanggil langsung. Yang berubah adalah situs mana yang boleh membaca data atas nama pengguna Anda.

SSRF — Server Diperalat Mengetuk Pintu Dalam

// Rentan — URL dari pengguna diambil apa adanya
app.post("/ambil-pratinjau", async (req, res) => {
    const hasil = await fetch(req.body.url);
    res.send(await hasil.text());
});
🚫 Target favorit SSRF adalah layanan metadata cloud

Server aplikasi biasanya dapat menjangkau alamat yang tertutup dari internet. Permintaan ke http://169.254.169.254/latest/meta-data/iam/security-credentials/ mengembalikan kredensial IAM sementara milik instans — dan dengan itu penyerang memperoleh hak yang sama seperti aplikasi Anda di cloud. Alamat internal lain seperti panel admin, Redis, dan Elasticsearch juga terbuka lewat jalur yang sama.

Menahan SSRF

import dns from "dns/promises";
import net from "net";

const RENTANG_TERLARANG = [
    "127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12",
    "192.168.0.0/16", "169.254.0.0/16", "::1/128", "fc00::/7"
];

async function urlAman(masukan) {
    const u = new URL(masukan);
    if (!["http:", "https:"].includes(u.protocol)) throw new Error("skema ditolak");

    const alamat = await dns.resolve4(u.hostname);
    for (const ip of alamat) {
        if (diRentangTerlarang(ip)) throw new Error("alamat internal ditolak");
    }
    return { url: u, ip: alamat[0] };      // sambungkan ke IP ini, bukan resolusi ulang
}
🚫 Memeriksa lalu mengambil membuka celah balapan DNS

Bila kode memvalidasi nama host, lalu memanggil fetch yang menyelesaikan DNS sekali lagi, penyerang dapat mengendalikan server DNS yang menjawab alamat publik pada pemeriksaan pertama dan 169.254.169.254 pada pemeriksaan kedua. Celah ini dikenal sebagai DNS rebinding. Solusinya adalah menyambung langsung ke alamat IP yang sudah divalidasi, bukan menyelesaikan nama host untuk kedua kalinya.

Lapis tambahan yang lebih kuat:
- Jalankan pengambilan URL lewat proksi keluar dengan daftar putih
- Tempatkan layanan itu pada jaringan tanpa akses ke rentang internal
- Matikan pengikutan pengalihan, atau validasi ulang setiap lompatan
- Batasi ukuran balasan dan waktu tunggu

Pemeriksaan Cepat untuk Endpoint API

□ Autentikasi diperiksa — bukan hanya keberadaan token, tapi keabsahannya
□ Otorisasi memeriksa kepemilikan sumber daya, bukan sekadar peran
□ Masukan divalidasi dengan skema ketat yang menolak field asing
□ Laju permintaan dibatasi, terutama pada endpoint mahal
□ Galat tidak membocorkan jejak tumpukan atau struktur internal
□ Balasan tidak menyertakan field sensitif yang tidak diminta
□ Aksi penting tercatat di log audit beserta identitas pelakunya