logo

Database

Server-side request forgery (SSRF)

Need

Restriction of server-side outbound requests to trusted destinations

Context

• Usage of Kotlin 1.9+ on the JVM for building web applications

• Usage of the Ktor server framework

• Usage of java.net.HttpURLConnection for outbound requests

Description

1. Non compliant code

import io.ktor.server.application.Application
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
import java.net.URI

fun Application.previewRoutes() {
    routing {...

The `/preview` route below opens the URL received in the `url` query parameter with `URL(...).openConnection()` and returns the response body to the caller. The server makes the request from its own network position, so the caller can point it at addresses that are not reachable from the internet: the cloud metadata service at `169.254.169.254`, which can return temporary credentials, internal admin panels, other services in the cluster or ports on `localhost`. Because `URL` also supports schemes such as `file:` and `jar:`, the same parameter can read local files. The body is returned to the caller, so the attacker also sees what those services answer. Clients such as `RestTemplate`, `WebClient` or OkHttp called with URLs built from request data have the same problem, as do servers that open sockets on ports chosen by the caller.

2. Steps

• Do not open `URL`, `HttpURLConnection`, `RestTemplate`, `WebClient` or OkHttp requests with URLs built from request data; map caller choices to URLs defined on the server.

• If a host must come from the request, parse it with `URI` and accept only `https`, no user information, the expected port and hosts from an explicit allowlist.

• Disable automatic redirects, or validate every redirect target against the same allowlist.

• Never open sockets on hosts or ports chosen by the caller.

• Block egress from the service to link-local, loopback and private address ranges at the network layer, including the cloud metadata endpoint.

3. Secure code example

import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
import java.net.HttpURLConnection
import java.net.URI...

The corrected route only fetches URLs that pass `allowedUri`. The function parses the value with `URI`, and accepts it only when the scheme is `https`, there is no user information, the port is the default one and the host is in `ALLOWED_HOSTS`. Values such as `http://169.254.169.254/`, `file:///etc/passwd`, `https://localhost:8443` or `https://api.example.com@evil.example` are rejected with `400 Bad Request`. The connection is opened with `instanceFollowRedirects = false`, so an allowed host cannot redirect the server to an internal address, and with connection and read timeouts, so a slow target cannot tie up the worker. For database, cache and socket connections, the equivalent fix is to take hosts and ports from configuration, never from the request.