flawopen.com/ssrf/Kotlin

● CWE-918 · Tinggi
Riset Keamanan · FlawOpen

Server-Side Request Forgery in Kotlin

How HTTP clients in Kotlin/Ktor allow SSRF, and how to enforce IP boundaries.

💡 Penjelasan Sederhana (ELI5)

Bayangkan mengirim kurir kantor untuk mengambil paket di toko umum, tetapi memberinya alamat brankas terkunci di ruang manajer. Karena kurir memiliki lencana akses internal, dia membuka brankas dan menyerahkan rahasia perusahaan kepada Anda.

Konsep Kunci & Istilah

Web Application Security
Komponen arsitektur utama yang terpengaruh oleh CWE-918.
CWE-918
Klasifikasi standar Common Weakness Enumeration (CWE) untuk ssrf-kotlin.
Defense-in-Depth
Verifikasi rekayasa berlapis dan isolasi batas waktu proses (runtime).

Alur Serangan Langkah demi Langkah

Step 1

Penerimaan URL Jarak Jauh Tidak Tepercaya

Aplikasi menerima parameter URL dari pengguna untuk mengunduh avatar, webhook, atau pratinjau dokumen.

Step 2

Pengalamatan Jaringan Internal

Penyerang memasukkan alamat loopback atau layanan metadata cloud (misal: http://169.254.169.254/latest/meta-data/).

Step 3

Koneksi Keluar Soket Tanpa Filter

Klien HTTP server mengirim permintaan dari dalam VPC privat tanpa memeriksa rentang subnet privat.

Step 4

Pencurian Kredensial Cloud IAM

Layanan metadata internal mempercayai permintaan dan mengembalikan token kredensial IAM sementara ke penyerang.

Kode Sumber: Rentan vs Aman

✕ IMPLEMENTASI RENTAN
// VULNERABLE: Naive java.net.URL openStream()
import java.net.URL;
import java.io.InputStream;
import javax.servlet.http.*;

public class ProxyServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String targetUrl = req.getParameter("url");
        // Attacker input: http://169.254.169.254/latest/meta-data/
        try {
            URL url = new URL(targetUrl);
            InputStream in = url.openStream();
            in.transferTo(resp.getOutputStream());
        } catch (Exception e) {
            resp.setStatus(500);
        }
    }
}
✓ PERBAIKAN AMAN & KUAT
// HARDENED: Resolve all IPs, assert non-private, and disable redirects
import java.net.*;
import java.net.http.*;
import java.time.Duration;
import javax.servlet.http.*;

public class ProxyServlet extends HttpServlet {
    private final HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(3))
        .followRedirects(HttpClient.Redirect.NEVER)
        .build();

    private boolean isSafeAddress(InetAddress addr) {
        return !addr.isLoopbackAddress() &&
               !addr.isSiteLocalAddress() &&
               !addr.isLinkLocalAddress() &&
               !addr.isAnyLocalAddress();
    }

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String targetUrl = req.getParameter("url");
        try {
            URI uri = URI.create(targetUrl);
            if (!uri.getScheme().equalsIgnoreCase("http") && !uri.getScheme().equalsIgnoreCase("https")) {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid scheme");
                return;
            }

            // 1. Resolve and validate all IP addresses
            InetAddress[] addresses = InetAddress.getAllByName(uri.getHost());
            for (InetAddress addr : addresses) {
                if (!isSafeAddress(addr)) {
                    resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Access to internal IP prohibited");
                    return;
                }
            }

            // 2. Fetch using hardened client
            HttpRequest request = HttpRequest.newBuilder(uri)
                .timeout(Duration.ofSeconds(4))
                .GET()
                .build();

            HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
            resp.getOutputStream().write(response.body());

        } catch (Exception e) {
            resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
        }
    }
}

Daftar Periksa Penguatan Sistem Rekayasa

References