flawopen.com/ssrf/Kotlin
How HTTP clients in Kotlin/Ktor allow SSRF, and how to enforce IP boundaries.
Stellen Sie sich vor, Sie schicken einen Boten, um ein Paket in einem öffentlichen Geschäft abzuholen, geben ihm aber die Adresse des Tresors im Büro des Geschäftsführers. Da der Bote einen internen Ausweis besitzt, öffnet er den Tresor und händigt Firmengeheimnisse aus.
Web Application SecurityCWE-918 betroffen ist.CWE-918Defense-in-DepthDie Anwendung akzeptiert externe URL-Parameter für Webhooks, Avatare oder PDF-Voransichten.
Der Angreifer übergibt eine Ziel-URL auf Cloud-Metadaten oder interne Loopback-Adressen (z. B. http://169.254.169.254/latest/meta-data/).
Der HTTP-Client des Servers sendet die Anfrage aus der geschützten VPC, ohne die Ziel-IP gegen private Subnetzbereiche abzugleichen.
Der interne Metadatendienst vertraut der Server-Anfrage und liefert temporäre IAM-Schlüssel und Tokens zurück.
// 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);
}
}
}
// 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);
}
}
}
InetAddress.getAllByName().addr.isSiteLocalAddress() && !addr.isLinkLocalAddress().