SSRF

SSRF: server dimanfaatkan menjadi proxy permintaan tanpa disadari.

Daftar isi

Server-Side Request Forgery (SSRF) adalah kerentanan ketika penyerang membuat server mengirim permintaan ke alamat pilihan penyerang, dan serverlah yang berdiri di balik permintaan tersebut. Pada OWASP Top 10:2025, SSRF yang pada edisi 2021 berdiri sendiri sebagai A10 kini digabungkan ke dalam A01 Broken Access Control karena sasarannya adalah sumber daya internal yang dilindungi. Topik ini memodelkan fitur pratinjau URL yang rentan, memperlihatkan cara server dijadikan proxy, lalu menutup celahnya menggunakan allowlist (daftar izin) skema dan host.

Server sebagai proxy yang tidak disadari#

Fitur seperti pratinjau tautan, impor dari URL, atau pengambilan gambar dari alamat luar membuat server menjalankan permintaan atas nama penggunanya. Apabila URL diambil mentah dari masukan pengguna, penyerang dapat mengarahkan permintaan itu ke alamat yang tidak terjangkau dari luar, misalnya loopback 127.0.0.1, rentang privat 10.0.0.0/8, atau endpoint metadata cloud 169.254.169.254. Browser penyerang tidak dapat mencapai jaringan internal, sedangkan server berada di dalam atau dekat dengan jaringan tersebut, sehingga server berfungsi sebagai proxy tanpa disadari.

Target yang paling bernilai adalah layanan tanpa autentikasi yang hanya mendengarkan pada jaringan dalam. Contoh nyatanya adalah endpoint metadata cloud pada 169.254.169.254 yang menyimpan token kredensial untuk instance itu sendiri. Diagram berikut memperlihatkan alur yang tidak dapat dilakukan penyerang secara langsung tetapi berhasil melalui server aplikasi.

Penyerang(browser)url = /admin pada 127.0.0.1Server aplikasifitur pratinjau URLpermintaan dikirim dari serverLayanan internalloopback / metadataakses langsung dari luar tidak dapat mencapai jaringan internal
Alur SSRF melalui fitur pratinjau URL

Cacat ini muncul setiap kali alamat yang diambil berasal dari masukan pengguna, bukan hanya pada pratinjau tautan. Webhook yang memanggil URL milik pengguna, fitur impor dokumen dari tautan, dan pengambilan gambar profil dari alamat luar adalah contoh yang setara. Nama host internal pun berbahaya karena DNS di jaringan server dapat menyelesaikan nama yang tidak dikenal dari luar, misalnya service-admin.internal. Dengan demikian, penyaringan yang hanya menilai bentuk teks alamat tanpa menilai posisi server akan selalu menyisakan celah.

Simulasi fitur pratinjau URL#

Skrip berikut menjalankan dua peran sekaligus. Sebuah server kecil pada loopback menyimulasikan layanan admin internal yang hanya boleh diakses dari jaringan dalam, lengkap dengan token pada responsnya. Fungsi ambil_url() adalah fitur pratinjau versi cacat yang mengambil URL apa pun yang dikirim pengguna. Simpan berkas berikut sebagai ssrf.py, lalu jalankan dengan perintah python3 ssrf.py.

python ssrf.py
"""Simulasi SSRF pada fitur pratinjau URL.

Sebuah server kecil di loopback menyimulasikan layanan internal yang
hanya boleh diakses dari jaringan dalam. Fitur pratinjau versi cacat
mengambil URL apa pun yang dikirim pengguna, sehingga permintaan ke
endpoint internal dikirim dari server, bukan dari browser penyerang.
"""

import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib import request

OPENER = request.build_opener(request.ProxyHandler({}))
permintaan_terakhir = {}


class LayananInternal(BaseHTTPRequestHandler):
    def do_GET(self):
        permintaan_terakhir["path"] = self.path
        if self.path == "/admin":
            isi = json.dumps({"layanan": "admin", "token_internal": "TOKEN-RAHASIA-7F3A"}).encode()
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(isi)
        else:
            self.send_response(404)
            self.end_headers()

    def log_message(self, format, *args):
        pass  # matikan log bawaan agar keluaran bersih


def ambil_url(url):
    """Fitur pratinjau versi cacat: mengambil URL apa pun tanpa penyaringan."""
    with OPENER.open(url, timeout=5) as respons:
        return respons.read().decode()


def utama():
    server = ThreadingHTTPServer(("127.0.0.1", 0), LayananInternal)
    benang_server = threading.Thread(target=server.serve_forever, daemon=True)
    benang_server.start()
    try:
        print("== Simulasi SSRF pada fitur pratinjau URL ==")
        print("[server internal] endpoint /admin berjalan pada loopback (127.0.0.1)")
        url_penyerang = f"http://127.0.0.1:{server.server_address[1]}/admin"
        print("[penyerang] mengirimkan url: http://127.0.0.1:<port-layanan>/admin")
        isi = ambil_url(url_penyerang)
        print("[fitur pratinjau] permintaan dikirim DARI server, bukan dari browser")
        print(f"[server internal] menerima {permintaan_terakhir['path']} dan menjawab 200")
        print("Isi yang kembali lalu ditampilkan kepada penyerang:")
        print(isi)
    finally:
        server.shutdown()
        server.server_close()


utama()
text Keluaran
== Simulasi SSRF pada fitur pratinjau URL ==
[server internal] endpoint /admin berjalan pada loopback (127.0.0.1)
[penyerang] mengirimkan url: http://127.0.0.1:<port-layanan>/admin
[fitur pratinjau] permintaan dikirim DARI server, bukan dari browser
[server internal] menerima /admin dan menjawab 200
Isi yang kembali lalu ditampilkan kepada penyerang:
{"layanan": "admin", "token_internal": "TOKEN-RAHASIA-7F3A"}

Penyerang tidak pernah berada di jaringan internal dan tidak mengetahui token apa pun sebelumnya. Seluruh pekerjaan dilakukan oleh fitur pratinjau, yaitu mengambil URL yang dikirim lalu menampilkan isi responsnya kembali. Port layanan internal sengaja tidak dicantumkan pada keluaran karena berubah setiap eksekusi, dan pada serangan nyata port itu ditemukan dengan memindai port-port yang umum digunakan layanan internal. Pola ini berlaku sama terhadap 169.254.169.254, hanya saja targetnya adalah metadata kredensial instance cloud.

Perbaikan: allowlist skema dan host#

Penyaringan berbasis blok mudah ditembus karena satu alamat dapat ditulis dengan banyak bentuk, misalnya notasi desimal untuk 127.0.0.1 atau tanda kurung siku untuk IPv6. Pendekatan yang bertahan adalah allowlist, yaitu menolak semua URL secara baku dan hanya menerima skema, host, serta rentang alamat yang terbukti diperlukan fitur. Urutan pemeriksanya penting karena host perlu diselesaikan menjadi alamat IP lebih dahulu sebelum rentangnya dapat dinilai. Simpan berkas berikut sebagai ssrf-perbaikan.py, lalu jalankan dengan perintah python3 ssrf-perbaikan.py.

python ssrf-perbaikan.py
"""Perbaikan fitur pratinjau URL dengan allowlist skema dan host.

Setiap URL diperiksa sebelum diambil: host diselesaikan menjadi alamat
IP, alamat loopback, link-local (termasuk metadata cloud), dan rentang
privat ditolak, host harus terdaftar dalam allowlist, lalu skema harus
https. Penyelesaian nama disimulasikan melalui tabel agar demo
berjalan deterministik tanpa jaringan.
"""

import ipaddress
from urllib.parse import urlparse

DNS_SIMULASI = {
    "rekan.example": "45.60.12.3",    # host mitra dengan alamat publik (simulasi)
    "rahasia.internal": "10.0.0.5",   # host pada jaringan privat
}

SKEMA_DIIZINKAN = {"https"}
HOST_DIIZINKAN = {"rekan.example"}


def url_diizinkan(url):
    """Mengembalikan pasangan (keputusan, alasan) untuk sebuah URL kandidat."""
    bagian = urlparse(url)
    host = bagian.hostname or ""
    if host in DNS_SIMULASI:
        alamat = DNS_SIMULASI[host]
    else:
        try:
            alamat = str(ipaddress.ip_address(host))
        except ValueError:
            return False, "host tidak ada dalam allowlist (deny by default)"
    ip = ipaddress.ip_address(alamat)
    if ip.is_loopback:
        return False, "host loopback"
    if ip.is_link_local:
        return False, "alamat link-local, termasuk metadata cloud 169.254.169.254"
    if ip.is_private:
        return False, "alamat pada rentang privat"
    if host not in HOST_DIIZINKAN:
        return False, "host tidak ada dalam allowlist (deny by default)"
    if bagian.scheme not in SKEMA_DIIZINKAN:
        return False, f"skema '{bagian.scheme}' tidak ada dalam allowlist"
    return True, "skema https, host terdaftar, dan alamat IP-nya publik"


KANDIDAT = [
    "https://rekan.example/laporan",
    "http://rekan.example/laporan",
    "http://127.0.0.1:8731/admin",
    "http://169.254.169.254/latest/meta-data/",
    "http://10.0.0.5/arsip",
    "http://[::1]/admin",
    "https://rahasia.internal/berkas",
]

print("== Perbaikan: allowlist skema dan host ==")
for url in KANDIDAT:
    keputusan, alasan = url_diizinkan(url)
    kata = "DITERIMA" if keputusan else "DITOLAK"
    print(f"{url:<42} -> {kata} ({alasan})")
text Keluaran
== Perbaikan: allowlist skema dan host ==
https://rekan.example/laporan              -> DITERIMA (skema https, host terdaftar, dan alamat IP-nya publik)
http://rekan.example/laporan               -> DITOLAK (skema 'http' tidak ada dalam allowlist)
http://127.0.0.1:8731/admin                -> DITOLAK (host loopback)
http://169.254.169.254/latest/meta-data/   -> DITOLAK (alamat link-local, termasuk metadata cloud 169.254.169.254)
http://10.0.0.5/arsip                      -> DITOLAK (alamat pada rentang privat)
http://[::1]/admin                         -> DITOLAK (host loopback)
https://rahasia.internal/berkas            -> DITOLAK (alamat pada rentang privat)

Setiap keputusan pada keluaran disertai alasannya, dan tidak ada satu pun alamat internal yang lolos. Pemeriksaan sekali jalan masih bisa dilewati DNS rebinding, yaitu teknik ketika sebuah nama domain menjawab alamat publik pada saat diperiksa lalu berganti ke alamat internal pada saat diambil. Karena itu, host diselesaikan satu kali, dinilai, kemudian alamat hasil penyelesaian itulah yang digunakan saat permintaan benar-benar dikirim. Alternatif lain yang setara adalah mengarahkan seluruh permintaan keluar melalui proxy egress yang menyaring tujuannya.

Pertahanan dapat ditambahkan di luar kode aplikasi. Layanan internal ditempatkan pada segmen jaringan terpisah sehingga server aplikasi tidak dapat mencapai semuanya, dan metadata instance pada cloud modern dapat diwajibkan menggunakan sesi, misalnya IMDSv2 yang menuntut permintaan token terlebih dahulu sebelum kredensial dikembalikan. Lapisan jaringan ini tidak menggantikan allowlist, tetapi mengecilkan dampak apabila allowlist keliru.

Kesalahan umum#

Kesalahan berikut sering ditemukan pada fitur pengambilan URL.

  • Memeriksa nama host lalu mengambil URL dengan nama yang sama lagi. Nilai DNS dapat berubah di antara keduanya, dan jarak itulah yang dieksploitasi DNS rebinding.
  • Melarang alamat tertentu satu per satu. Bentuk penulisan yang bervariasi dan rentang yang lebar membuat blok per alamat hampir pasti tidak mencukupi.
  • Mengikuti pengalihan secara otomatis. Rantai pengalihan dapat berujung di alamat internal yang sebenarnya sudah ditolak di awal.
  • Mengizinkan skema apa pun. Skema selain http dan https membuka bentuk serangan lain yang sama sekali berbeda.

Ringkasan#

  • SSRF terjadi ketika server mengambil URL dari masukan pengguna tanpa penyaringan, sehingga menjadi proxy menuju jaringan internal.
  • Endpoint metadata cloud pada 169.254.169.254 adalah target nyata karena menyimpan kredensial instance.
  • Perbaikannya adalah allowlist skema dan host dengan penilaian terhadap alamat IP hasil penyelesaian, penonaktifan pengalihan, serta mitigasi DNS rebinding.

Referensi#