SSRF (Server-Side Request Forgery) — Security

Apa itu SSRF? Server-Side Request Forgery (SSRF) adalah serangan dimana attacker memaksa server (bukan browser) untuk membuat HTTP request ke tujuan yang…

SSRF (Server-Side Request Forgery) terjadi kalau penyerang bisa menyuruh server-mu mengirim request ke alamat pilihan penyerang. Bayangin resepsionis kantor yang mau mengambilkan paket dari alamat mana pun yang kamu tulis di kertas. Kamu menulis "ruang arsip direksi, lantai 5". Kamu sendiri gak boleh masuk ke sana, tapi resepsionis itu orang dalam, jadi pintunya terbuka untuknya, dan dia membawakan isinya ke kamu.

🧪 Tebakan cepat: server-mu di AWS EC2 mengambil gambar dari URL kiriman user untuk membuat preview. Penyerang mengirim http://169.254.169.254/latest/meta-data/iam/security-credentials/. Apa yang didapat penyerang? Tebak dulu, jawabannya ada di bagian "Kenapa server jadi senjata".

Kenapa server jadi senjata

Fitur yang mengambil URL dari user ada di mana-mana: preview link, avatar dari URL, webhook. Versi rentannya seperti ini:

app.post('/fetch-preview', async (req, res) => {
  const response = await fetch(req.body.url) // RENTAN: URL dari user, langsung di-fetch
  res.send(Buffer.from(await response.arrayBuffer()))
})

Server biasanya berada di jaringan internal yang gak bisa dijangkau dari internet. Lewat SSRF, penyerang meminjam posisi itu:

Jadi di EC2 dengan IMDSv1, request tadi mengembalikan nama role IAM-nya, dan satu request lagi ke .../security-credentials/<nama-role> mengembalikan kredensial AWS sementara: AccessKeyId, SecretAccessKey, dan Token. Itu jawaban tebakannya. Ini persis yang terjadi pada kebocoran Capital One tahun 2019: SSRF lewat WAF yang salah konfigurasi, kredensial dari metadata, lalu data sekitar 106 juta orang diunduh dari S3.

Membaca URL dengan new URL

Jangan memeriksa URL dengan mencari teks. Biarkan new URL() memecahnya, persis seperti nanti fetch membacanya:

URL protocol hostname
https://images.cdn.com/pic.jpg https: images.cdn.com
http://images.cdn.com/pic.jpg http: images.cdn.com
https://evil.com/x https: evil.com
file:///etc/passwd file: (kosong)
https://IMAGES.CDN.COM/x https: images.cdn.com
bukan-url melempar TypeError: Invalid URL

Allowlist: cara paling efektif

function isUrlSafeForFetch(url, allowlist) {
  try {
    const parsed = new URL(url)
    if (parsed.protocol !== 'https:') return false // tolak http:, file:, gopher:, dll
    const host = parsed.hostname.toLowerCase()
    return allowlist.some((h) => h.toLowerCase() === host)
  } catch {
    return false // bukan URL yang valid
  }
}

const ok = ['images.cdn.com', 'storage.googleapis.com']

app.post('/fetch-preview', async (req, res) => {
  if (!isUrlSafeForFetch(req.body.url, ok)) return res.status(400).send('URL not allowed')
  const response = await fetch(req.body.url, { redirect: 'error' })
  res.send(Buffer.from(await response.arrayBuffer()))
})
url Langkah yang menghentikan Hasil
https://images.cdn.com/pic.jpg lolos semua true
http://images.cdn.com/pic.jpg protocol http: false
https://evil.com/x evil.com gak ada di allowlist false
file:///etc/passwd protocol file: false
bukan-url new URL melempar error, masuk catch false
https://IMAGES.CDN.COM/x lolos semua, host-nya images.cdn.com true

Lapisan tambahan

IMDSv1 vs IMDSv2

IMDS adalah layanan metadata di EC2. IMDSv1 menjawab satu GET biasa, jadi satu SSRF sudah cukup. IMDSv2 mewajibkan session token: dapatkan dulu lewat PUT, lalu kirim di header setiap GET:

TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/

Kebanyakan SSRF cuma bisa mengirim GET tanpa header tambahan, jadi gak bisa mendapat token. GET tanpa token ke instance yang mewajibkan IMDSv2 dijawab 401 Unauthorized. Wajibkan dengan aws ec2 modify-instance-metadata-options --instance-id i-xxx --http-tokens required.

⚠️ Jebakan yang sering ketemu

TL;DR: SSRF berarti penyerang menyuruh server-mu me-request alamat pilihannya, termasuk metadata cloud 169.254.169.254 (di EC2 dengan IMDSv1, hasilnya kredensial AWS sementara), layanan internal, dan file://. Cegah dengan allowlist: parse dengan new URL di dalam try...catch (gagal parse berarti false), wajibkan protocol === 'https:', dan cocokkan hostname huruf kecil dengan ===. Tambahkan redirect: 'error', blokir IP private setelah resolve DNS kalau allowlist gak mungkin, batasi egress, dan wajibkan IMDSv2, yang butuh session token dari PUT sehingga SSRF biasa yang cuma bisa GET gak bisa membaca metadata.

Yang akan kamu pelajari