flawopen.com/ssrf/Kotlin
How HTTP clients in Kotlin/Ktor allow SSRF, and how to enforce IP boundaries.
想象一下,你派办公室助理去公共快递站取包裹,却故意给他留了总经理办公室带锁保险箱的内部地址。因为助理佩戴着公司内部通行工牌,保安直接放行,他便打开保险箱将公司核心机密交给了你。
Web Application SecurityCWE-918.CWE-918CWE-918):Standard Common Weakness Enumeration classification for ssrf-kotlin.Defense-in-Depth应用程序接受用户提交的外部 URL 用于拉取头像、Webhook 推送或生成网页预览。
攻击者输入指向内网回环地址或云厂商元数据接口(如 http://169.254.169.254/latest/meta-data/)的地址。
服务器内部 HTTP 客户端直接从受信任的私有 VPC 发起网络请求,未做 IP 白名单与私有地址段校验。
内部元数据服务信任来自同主机的请求,回传临时 IAM 访问凭据、Kubernetes Token 或管理后台内容。
// 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()。